From ddd4aa0397f5256953bdf5de6c70435837b8dc04 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 2 Feb 2026 13:27:35 +0000 Subject: [PATCH] Autosave: 20260202-132735 --- config/__pycache__/settings.cpython-311.pyc | Bin 5813 -> 5917 bytes config/__pycache__/urls.cpython-311.pyc | Bin 1092 -> 1195 bytes config/settings.py | 8 +- config/urls.py | 3 +- core/__pycache__/admin.cpython-311.pyc | Bin 3678 -> 5695 bytes .../context_processors.cpython-311.pyc | Bin 1404 -> 1331 bytes core/__pycache__/models.cpython-311.pyc | Bin 23883 -> 25636 bytes core/__pycache__/urls.cpython-311.pyc | Bin 6732 -> 7245 bytes core/__pycache__/views.cpython-311.pyc | Bin 57973 -> 67560 bytes core/admin.py | 42 +- core/context_processors.py | 10 +- ..._by_purchasepayment_created_by_and_more.py | 51 +++ ...asepayment_payment_method_name_and_more.py | 43 +++ ...ayment_created_by_and_more.cpython-311.pyc | Bin 0 -> 2742 bytes ...yment_method_name_and_more.cpython-311.pyc | Bin 0 -> 2284 bytes core/models.py | 22 +- core/templates/base.html | 206 ++++++---- core/templates/core/index.html | 14 +- core/templates/core/inventory.html | 1 + core/templates/core/invoice_create.html | 15 +- core/templates/core/invoice_detail.html | 20 +- core/templates/core/invoices.html | 24 +- core/templates/core/pos.html | 20 +- core/templates/core/purchase_create.html | 13 +- core/templates/core/purchase_detail.html | 24 +- core/templates/core/purchase_returns.html | 16 +- core/templates/core/purchases.html | 24 +- core/templates/core/quotations.html | 8 +- core/templates/core/sales_returns.html | 16 +- core/templates/core/settings.html | 359 +++++++++++++----- core/templates/core/users.html | 119 ++++++ core/templates/registration/login.html | 56 +++ core/urls.py | 8 +- core/views.py | 301 ++++++++++++--- static/css/custom.css | 39 +- staticfiles/css/custom.css | 39 +- 36 files changed, 1204 insertions(+), 297 deletions(-) create mode 100644 core/migrations/0010_purchase_created_by_purchasepayment_created_by_and_more.py create mode 100644 core/migrations/0011_paymentmethod_purchasepayment_payment_method_name_and_more.py create mode 100644 core/migrations/__pycache__/0010_purchase_created_by_purchasepayment_created_by_and_more.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0011_paymentmethod_purchasepayment_payment_method_name_and_more.cpython-311.pyc create mode 100644 core/templates/core/users.html create mode 100644 core/templates/registration/login.html diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 88d5360607371f6a7e63876638bb3ed2f2d137ee..ab578e63a03bc8677da8ab826d2aa5774f2c8491 100644 GIT binary patch delta 193 zcmdn0J6Df)IWI340}!Y*G-U3X$ScXj#JW-AD~q6KlyQn@lu3$LlxYfcFoUM|W+m2t zT%x}i^{WK*6O)tkOY=&K^>gymGxPK(D+nksnr(Iz&}B06@pt$1iw|;j@eFcx4v7y9 z^0_4p6!Q-aK^5eLCmtP7=^% kntWJDhpPannh}VLIXC|l`p+!H!60UOfk6m@iUff&06(7=p8x;= diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 1b7a2c8ac28ebf995bef8653843bf171445911a8..fe03dcc3a9c87784918a0c8c761eb0026373191f 100644 GIT binary patch delta 237 zcmX@Yv6@qTIWI340}u!`G-PHmF)%y^abSQG%J|$hQGGKH6GJLX3Kx)MPT`)oPga*D ziaV7piwCB-$S;)}D9Mq+0~FZST9?WBU=rv#gAu1FjeLzRLwN3 zpHL&JB{%(n>gWe8e&ohKw+zQkJ>)H|({N~^V>>NV=Wcqh({48n8pZe2rcTdp#yxUCv&|JmY??6%s_w;L_4Z;QxYZy)g`dPhKx2KG8ZH@JQ*?R4**tz7>8 z-IL`TC-H^BAK&m_8S|6hNzRM39QG6WC+235HlA;U@qC3u-K>4s;*M}Z?OB{(0y|0; zUjwWI-T;WtUcqS7hj|*+3h$9SyMG`=q!z_(~rt0to? zcV|T0zciW-ZK}MjtF&>vfQP+u4TOLS-8S4f>XkZ0viO_s-Lvve_dc$He;^<2ADk4z zVT6AulyORRZ_E3s9!H*%PYzoa{nJg=XjWzZ4sr0C1eE4c=JB^75Q^?^F$X|)*oy?C zcDzM)l&_AO0MTd@UYdz{zfSqt2u0mOqh&c1pL$#8cFP(l&flb^u!_IO9AKU9^Kb&* z98Ew-y{2}>v0~(_ILwzDL|Kc}>yNa;0b-l{0Z$wLRJ0P~QPyJ=VMaSZ78_O+R+P&- zR@1O_9w)AdM4h2&H$jCMl&E3(+xkgPwspHo1xPgq}Q*m6W%cGEm#AGOlcB zZo#k~x2U+(t&X{mw%YI;Y|f#6*sQB&i>kXj(^d`5e@NQYDLb~CZE#(*1=V>b^Sh(~ zzeiB3rXzRY4g^GeV1=79M5yhsOLid!K=Roe1f!eMLUFl9#bx%=$=Z#R!cO-#u0b&I z)|hvKPBET*gN`~ZF@D$fJj4G&Jp49+o3_1U2ET*3j{$+9R9Zh7o2WgDu06u?P~;!t zO#q5kDDpp+gqb(HcX17DgXk}?tq>e$2F4}axv%|`ju&+P5mXWO1*Y-)G*{!FVkM;b zXP5(E(@!Y=g%_Wp;t-8i{G~;6c6iJhXiaT^ixb$S$|B<;OVnSXDBwN1CrW2+v~)u1 zHFYfcNi#V5d3y4{5z`0}6myz#IVQE{;Uu=zY8lz^Q{{NAm>&H_i0}IqS_@JDDX{G5JwaEAU9 z*XbK5Huit0bM<44u`_A?Wc;r$rr0bxoaoK1KgYrg09Y;r@=2d1b;=fdY_TuL z*xP++5{}>Ll{TJZ;ROIJq`N1d^;t$N+Jr@62rSx!MVq4*ZT16;0v5(@!J;sDEVKZN zZVg!=8d#J+Wve~53crL21nHAXZ>b8u3|ZC!`J~JWFdz)apZ6+nA|wSE;H{>E(Qpo< zCyCzT)`YO{s~~QZ&K3di;x_8>LEIo8<#Do7hN)u^c$}=Hg`uw?6?i;z$|$pzk%?O% z2m{Q%3QM-YvIUlvKt5o}o-&$UfyW;(_&5rW_1=X|xcvb`^d2yfT^zm^#IJD5C?0FC oW_xnAw|p(kwmy6nJXU7=eP1}U*CblVlW`AISUg6KQ{0~PC$N&o-= literal 3678 zcmbtWJ8#=o6uy_FBubWK$CeyBX&eVl+^UV^xN$K_q8UIyN$KO`p8N4UzI(_&Yqbi2 z?XRDE)}TPhKX{RB#rWj-L?YxhF^NfS;!urJVoIiLOOC9`j-n|}K`S^#t>~1rlA~&> zQ`X8(MXNYd+7u-cVf1-5A!YI#O2|9-N1H_M6H|Fb%mP@|lDltYuz`QHy@A8K#EjD!#zGFG3XJ868D}z##T>>8FiuU(crL?O%3-VmV{Kx_*)aEYm@^G? z=8^=D=U!`Mo~!0OcLq4lP5j(uhOv^vI17w(6En_d7^iX=8^G9{nDM+hzeQT-KVVW? zR1^%;u{s}6wXL#H?-@SZarvH5o^~u>l%8n1fK6Yj0<%u=yq)j&Xg^~W^fJo zz-Nw7p9H+UYj{l5w)Q-@v&DSh>g=?W{NoW8T(})yfWvFTh(=AKNhZ}~TzVGO6jK2Q z3!;|z@&kI&`=i@U3b8Jo9Vi!>iRE% zVaFp?+wy$fw7jlu?1{N9XI-}&*buy_KeL!^dT+@w`uXqb+Af5>7HkI{KUiZgSv&Au zzV@5TUwB=k&DPp3XKP^+Z*=$gG|UM>ffH{TpqK9N=hl0V1{bgVarfZDR|m@Z=*Jh6 zDPws`+Q>0Rm(3I8M;2wQP@J1EczzB{MaA27xgVxr^R2>B{JeqCM3_fdKnNd;Mc|(# zP=*r^Cye@hyY~q9$R17e-OMW|lv36TaXG88lp>EyBOc4%i$D~*+-fsX2bHy7V5vI3 z{0iLIY!&mu6;olcpINpKqOEz%0F}m7&Zc4+$q}$$ zdzS!4Q=vooW>T@7;KUnGDILpg1X9AoZj(J?Pk0hj9gH zax^jpw?zRO1+yXRFMV!gU0nwYj)3ydWOGn$^fm_7T5kjQNDye6?=QwWE(oUVoO)bF zkE47lLb6Pdf6{iVzUv#dZa6M9B2j5`7B(xx&xt5p~)sZr?wqA;F38b{U1lAk5-$l1yMJj8=W2Z1}-mdZF3p!i()8R zkHhCJ;rLC@hg>MQxhs@JMi*@R#QdL>Iw56MlxQIf7QFuOZPi|ANcS+=KT$C{h>@L9gyXN@!x>#9{j)G zdpg}~zFOK>59wl`E)FG$t`Ess*x%?kt{&mQ3Br*~qzkFNN~D>PMLDF+K5Y&qO79HG zS=cZ1XO@p}-~<6#=2QD(!csb<3w^pUlw|t#ker46oBg>@k8t1w0a?KJ*uIjmj6}48 z5oIBbXayr$8I5RVm`0ScPITImY)~Y-oI5S-BL4xGhyG~* diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 7392a4c4d1efb57dc540755192a10398047eaaaf..268182e70b629d4e59130fb4250ed8130189f5d2 100644 GIT binary patch delta 373 zcmeyvwV5kuIWI340}yD=ZOHUzW?*;>;=lk4l<_$Z$e7NM!jQt4!;s4u#mLBz!j!_C z!xY5?rkSIdnHW-8fS5Upd9necibxb|3QG$^6k7@_kYrC`4rb6~n;0h}p3Dd{8%VPP zG0;ke&nXkuQ}2mnufPx=4= delta 460 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 delta 6924 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 YWi-GcuE7`tbTo*BI}0HMkpQ>n0q?~-WB>pF delta 19191 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 H%o6@@q`{<2 diff --git a/core/admin.py b/core/admin.py index a255268..49646aa 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,5 +1,13 @@ from django.contrib import admin -from .models import Category, Unit, Product, Customer, Supplier, Sale, SaleItem, Purchase, SystemSetting +from .models import ( + Category, Unit, Product, Customer, Supplier, + Sale, SaleItem, SalePayment, + Purchase, PurchaseItem, PurchasePayment, + Quotation, QuotationItem, + SaleReturn, SaleReturnItem, + PurchaseReturn, PurchaseReturnItem, + SystemSetting, PaymentMethod +) @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): @@ -13,7 +21,7 @@ class UnitAdmin(admin.ModelAdmin): @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ('name_en', 'name_ar', 'sku', 'price', 'stock_quantity', 'category', 'unit') - list_filter = ('category', 'unit') + list_filter = ('category', 'unit', 'is_active') search_fields = ('name_en', 'name_ar', 'sku') @admin.register(Customer) @@ -25,19 +33,41 @@ class CustomerAdmin(admin.ModelAdmin): class SupplierAdmin(admin.ModelAdmin): list_display = ('name', 'contact_person', 'phone') +@admin.register(PaymentMethod) +class PaymentMethodAdmin(admin.ModelAdmin): + list_display = ('name_en', 'name_ar', 'is_active') + class SaleItemInline(admin.TabularInline): model = SaleItem extra = 1 +class SalePaymentInline(admin.TabularInline): + model = SalePayment + extra = 1 + @admin.register(Sale) class SaleAdmin(admin.ModelAdmin): - list_display = ('id', 'customer', 'total_amount', 'created_at') - inlines = [SaleItemInline] + list_display = ('id', 'invoice_number', 'customer', 'total_amount', 'paid_amount', 'status', 'created_at') + list_filter = ('status', 'created_at') + inlines = [SaleItemInline, SalePaymentInline] @admin.register(Purchase) class PurchaseAdmin(admin.ModelAdmin): - list_display = ('id', 'supplier', 'total_amount', 'created_at') - list_filter = ('supplier', 'created_at') + list_display = ('id', 'invoice_number', 'supplier', 'total_amount', 'paid_amount', 'status', 'created_at') + list_filter = ('supplier', 'status', 'created_at') + +@admin.register(Quotation) +class QuotationAdmin(admin.ModelAdmin): + list_display = ('quotation_number', 'customer', 'total_amount', 'status', 'created_at') + list_filter = ('status', 'created_at') + +@admin.register(SaleReturn) +class SaleReturnAdmin(admin.ModelAdmin): + list_display = ('return_number', 'customer', 'total_amount', 'created_at') + +@admin.register(PurchaseReturn) +class PurchaseReturnAdmin(admin.ModelAdmin): + list_display = ('return_number', 'supplier', 'total_amount', 'created_at') @admin.register(SystemSetting) class SystemSettingAdmin(admin.ModelAdmin): diff --git a/core/context_processors.py b/core/context_processors.py index 2769ebb..33b1942 100644 --- a/core/context_processors.py +++ b/core/context_processors.py @@ -1,6 +1,10 @@ from .models import SystemSetting import os -from django.utils import timezone +import time + +# Stabilize the timestamp to avoid cache-busting on every single request +# This will only change when the server restarts +STARTUP_TIMESTAMP = int(time.time()) def project_context(request): """ @@ -10,7 +14,7 @@ def project_context(request): return { "project_description": os.getenv("PROJECT_DESCRIPTION", ""), "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), - "deployment_timestamp": int(timezone.now().timestamp()), + "deployment_timestamp": STARTUP_TIMESTAMP, } def global_settings(request): @@ -20,4 +24,4 @@ def global_settings(request): settings = SystemSetting.objects.create() return {'site_settings': settings} except: - return {} + return {} \ No newline at end of file diff --git a/core/migrations/0010_purchase_created_by_purchasepayment_created_by_and_more.py b/core/migrations/0010_purchase_created_by_purchasepayment_created_by_and_more.py new file mode 100644 index 0000000..a36b326 --- /dev/null +++ b/core/migrations/0010_purchase_created_by_purchasepayment_created_by_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 5.2.7 on 2026-02-02 10:42 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_purchasereturn_purchasereturnitem_salereturn_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='purchase', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchases', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='purchasepayment', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_payments', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='purchasereturn', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_returns', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='quotation', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quotations', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='sale', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='salepayment', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sale_payments', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='salereturn', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sale_returns', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/migrations/0011_paymentmethod_purchasepayment_payment_method_name_and_more.py b/core/migrations/0011_paymentmethod_purchasepayment_payment_method_name_and_more.py new file mode 100644 index 0000000..b8298f0 --- /dev/null +++ b/core/migrations/0011_paymentmethod_purchasepayment_payment_method_name_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 5.2.7 on 2026-02-02 13:01 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_purchase_created_by_purchasepayment_created_by_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='PaymentMethod', + 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)')), + ('is_active', models.BooleanField(default=True, verbose_name='Active')), + ], + ), + migrations.AddField( + model_name='purchasepayment', + name='payment_method_name', + field=models.CharField(default='Cash', max_length=50, verbose_name='Payment Method Name'), + ), + migrations.AddField( + model_name='salepayment', + name='payment_method_name', + field=models.CharField(default='Cash', max_length=50, verbose_name='Payment Method Name'), + ), + migrations.AlterField( + model_name='purchasepayment', + name='payment_method', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_payments', to='core.paymentmethod'), + ), + migrations.AlterField( + model_name='salepayment', + name='payment_method', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sale_payments', to='core.paymentmethod'), + ), + ] diff --git a/core/migrations/__pycache__/0010_purchase_created_by_purchasepayment_created_by_and_more.cpython-311.pyc b/core/migrations/__pycache__/0010_purchase_created_by_purchasepayment_created_by_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44cd0636e7953dbb8dc2d563ea62ca3eea0a896e GIT binary patch literal 2742 zcmbuBO>Y}T7{_PF_Ih_?=N%ygl_-fwEF{}VTtEm2!D&cRyK0gmOvJF>nfRsq;_eH< z2Sg4WIQG^rfI_SGz(?S~iBph#u;$8%o1^4ZsS-1@yH1)GN7at)nP;Av`Tb|c`#kn< z`Md^LdGym^`7#1|6jUc+5Uwsb{gY4GQwVyjv|uRPl)Kx9z4^fK;>EO;`8Zj_py|axgJH zBU&e#nW%I6E8{%?MC_*yu>=80b<*)vrY^_!k;K&5CoD}^&x*P1v|+&zr7bT%!8~qegFssh@ilwgc0o zs$(OTNQh5p&LyT;Ve1y5(02+cmU4KSLBf+3b!NX!OiZU@Vps23#P;b->_Cynqm!{K zJOiDM`zUmHRJ|8CzQ|})8<`$W^P|xWuRS`!n^+~!JLd`-??&{T7>dZ%!ZcNo*f&B{ zL+o~Nz*ZufL+DXDqm2pS6n+}DzK^1YsEN@d~yRw6-yIWgS zbGjr-=8wQB&zDb~Qp>SODQEL&Mb~z5D1KCm-JZ*Tgha*jz4sRc;BfRB%0R7217+zKt*Vfd=GtI(;r_e**@cITzwi8U=g+VEmEC^j+kT}v1mHmW9J1yH ztidzhAl{q%4xWgbxtKl7rd90(uDd?kK^18eXrj<3O{z_$dx#a(l)armVx z@n3Ra72e^jD%= zTZNmPvkJcv=skhn6X?Bh=*4kp#yybMIB*kgbJiv_1Re`K7I-`kzi=hK!hsv`HfL?X zhQJ#FZwNdSymarMF3!q_8~o*#;)lDS&^N+hEaFBn{NGfJBhoFfX>=kQHynFE419I8 zLgWY$-(kEIIak2T%zCPP9Hy2}+z(in|CRE5Rt-~-&9(v@~9DAav@S#VJJ@mkFk@jHCDN;|l8HH0$otgETK#-y;v%7ER&CGk>`g5`YN40Wk6lfD($- zLK(0d0bq}QK`Ta~@C4+euNk|T7fHNsRIu(Ew&f7HZbM``huk!#fcph1Z~Q7E_ib|q>B{ere8!WiEf3umALP>x~_wo+$xRisw>^cO$+{=?Jzmr};BlE`5ZS z{+EB(Z^T$)>=T zUPq*lQ4LwIKcykv8d#*<==iKvF%74h=ch)0u9?WHxK+&J-xbjDRx;Q-jrEdI&J)FO zG`;K^8;Ho$yrn|A!9j|le0t=-X-KlO3b=@8f z{2pcC`3Sy9O~NW8%(#4no^p1wxg^dod!rnT^gFtVLY#rqI3yh!qIDOXEtREeqRR&6 zo9@ z{t4btXz&&ns&*X}nx&@YHVf!EDpNyn;hBxsorYdU1s1l#$xU0JwLTSwp_|n(*kKaF z{+*<2VYyE!j4=R0wrjQkq+W4U&E^i}$Tm$|*O z;b(5|O9x|KQS&V9Wt_u|<7XT%<+jsTyzHG9bAI;DzUVzz@v_CkY|+maz0{AV)(%Fz z?8;$w#m}yIsbV{w^>R}$*8JSm{@B3}UT*m?x9sPZz0~&|B|1FNiN=)p5uoIlk&X(K zEVJoqJ2T!+-)gHjc5eCVjhCPAm%PW&Q_-P{d=+*2Bqm15F(b^CP;Bh~iRBpDHI8<@ zt=?p4H(!qZwemC>l-ZmKb&U-uZm>_q+`(!8X Pr^b%h!tpt)+=l-EcEW0x literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 2c98bf5..697ec63 100644 --- a/core/models.py +++ b/core/models.py @@ -1,6 +1,7 @@ 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 class Category(models.Model): name_en = models.CharField(_("Name (English)"), max_length=100) @@ -58,6 +59,14 @@ class Supplier(models.Model): def __str__(self): return self.name +class PaymentMethod(models.Model): + name_en = models.CharField(_("Name (English)"), max_length=50) + name_ar = models.CharField(_("Name (Arabic)"), max_length=50) + is_active = models.BooleanField(_("Active"), default=True) + + def __str__(self): + return f"{self.name_en} / {self.name_ar}" + class Sale(models.Model): PAYMENT_TYPE_CHOICES = [ ('cash', _('Cash')), @@ -81,6 +90,7 @@ class Sale(models.Model): status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='paid') due_date = models.DateField(_("Due Date"), null=True, blank=True) notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sales") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): @@ -112,8 +122,10 @@ class SalePayment(models.Model): sale = models.ForeignKey(Sale, on_delete=models.CASCADE, related_name="payments") amount = models.DecimalField(_("Amount"), max_digits=15, decimal_places=3) payment_date = models.DateField(_("Payment Date"), default=timezone.now) - payment_method = models.CharField(_("Payment Method"), max_length=50, default="Cash") + payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_payments") + payment_method_name = models.CharField(_("Payment Method Name"), max_length=50, default="Cash") # Fallback notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_payments") def __str__(self): return f"Payment of {self.amount} for Sale #{self.sale.id}" @@ -135,6 +147,7 @@ class Quotation(models.Model): valid_until = models.DateField(_("Valid Until"), null=True, blank=True) terms_and_conditions = models.TextField(_("Terms and Conditions"), blank=True) notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="quotations") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): @@ -171,6 +184,7 @@ class Purchase(models.Model): status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='paid') due_date = models.DateField(_("Due Date"), null=True, blank=True) notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchases") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): @@ -202,8 +216,10 @@ class PurchasePayment(models.Model): purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE, related_name="payments") amount = models.DecimalField(_("Amount"), max_digits=15, decimal_places=3) payment_date = models.DateField(_("Payment Date"), default=timezone.now) - payment_method = models.CharField(_("Payment Method"), max_length=50, default="Cash") + payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_payments") + payment_method_name = models.CharField(_("Payment Method Name"), max_length=50, default="Cash") # Fallback notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_payments") def __str__(self): return f"Payment of {self.amount} for Purchase #{self.purchase.id}" @@ -214,6 +230,7 @@ class SaleReturn(models.Model): return_number = models.CharField(_("Return Number"), max_length=50, blank=True) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_returns") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): @@ -235,6 +252,7 @@ class PurchaseReturn(models.Model): return_number = models.CharField(_("Return Number"), max_length=50, blank=True) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_returns") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): diff --git a/core/templates/base.html b/core/templates/base.html index 9b79024..61ca302 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -29,6 +29,7 @@
+ {% if user.is_authenticated %} + {% endif %} -
+
@@ -170,7 +170,7 @@ {% if sale.payments.exists %} -
+
{% trans "Payment Records" %} / سجلات الدفع
@@ -179,6 +179,7 @@ + @@ -186,8 +187,15 @@ {% for payment in sale.payments.all %} - + + {% endfor %} @@ -199,13 +207,13 @@ {% if sale.notes %} -
+
{% trans "Internal Notes" %} / ملاحظات داخلية

{{ sale.notes }}

{% endif %} -
+

{% trans "Thank you for your business!" %} / شكراً لتعاملكم معنا!

{% trans "Software by Meezan" %} / برمجة ميزان

diff --git a/core/templates/core/invoices.html b/core/templates/core/invoices.html index 9ac35a1..a72ff3b 100644 --- a/core/templates/core/invoices.html +++ b/core/templates/core/invoices.html @@ -36,8 +36,7 @@
- - + @@ -51,13 +50,10 @@ - - {% empty %} - diff --git a/core/templates/core/pos.html b/core/templates/core/pos.html index 28d08db..dc07a9f 100644 --- a/core/templates/core/pos.html +++ b/core/templates/core/pos.html @@ -175,6 +175,16 @@ {% trans "Total" %}{{ site_settings.currency_symbol }}0.000 + +
+ + +
+ @@ -352,11 +362,15 @@ payBtn.disabled = true; payBtn.innerText = '{% trans "Processing..." %}'; + const totalAmount = cart.reduce((acc, item) => acc + item.line_total, 0); const data = { customer_id: document.getElementById('customerSelect').value, + payment_method_id: document.getElementById('paymentMethodSelect').value, items: cart, - total_amount: cart.reduce((acc, item) => acc + item.line_total, 0), - discount: 0 + total_amount: totalAmount, + paid_amount: totalAmount, + discount: 0, + payment_type: 'cash' }; fetch('{% url "create_sale_api" %}', { @@ -470,4 +484,4 @@ }); } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/purchase_create.html b/core/templates/core/purchase_create.html index 9170ad7..9892f93 100644 --- a/core/templates/core/purchase_create.html +++ b/core/templates/core/purchase_create.html @@ -122,6 +122,15 @@ +
+ + +
+
@@ -177,6 +186,7 @@ supplierId: '', invoiceNumber: '', paymentType: 'cash', + paymentMethodId: '{% if payment_methods.first %}{{ payment_methods.first.id }}{% endif %}', paidAmount: 0, dueDate: '', notes: '', @@ -243,6 +253,7 @@ total_amount: this.subtotal, paid_amount: actualPaidAmount, payment_type: this.paymentType, + payment_method_id: this.paymentMethodId, due_date: this.dueDate, notes: this.notes }; @@ -272,4 +283,4 @@ } }).mount('#purchaseApp'); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/purchase_detail.html b/core/templates/core/purchase_detail.html index d515b0e..4472c90 100644 --- a/core/templates/core/purchase_detail.html +++ b/core/templates/core/purchase_detail.html @@ -48,11 +48,15 @@
{{ purchase.invoice_number|default:purchase.id }}
-
+
{% trans "Issue Date" %} / تاريخ الإصدار
{{ purchase.created_at|date:"Y-m-d" }}
-
+
+
{% trans "Issued By" %} / صادرة عن
+
{{ purchase.created_by.username|default:"System" }}
+
+
{% trans "Due Date" %} / تاريخ الاستحقاق
{{ purchase.due_date|date:"Y-m-d"|default:"-" }}
@@ -152,7 +156,7 @@ {% if purchase.payments.exists %} -
+
{% trans "Payment History" %} / سجل الدفعات
{% trans "Date" %} / التاريخ {% trans "Method" %} / الطريقة {% trans "Amount" %} / المبلغ{% trans "User" %} / المستخدم {% trans "Notes" %} / ملاحظات
{{ payment.payment_date|date:"Y-m-d" }}{{ payment.payment_method }} + {% if payment.payment_method %} + {% if LANGUAGE_CODE == 'ar' %}{{ payment.payment_method.name_ar }}{% else %}{{ payment.payment_method.name_en }}{% endif %} + {% else %} + {{ payment.payment_method_name }} + {% endif %} + {{ settings.currency_symbol }}{{ payment.amount|floatformat:3 }}{{ payment.created_by.username|default:"System" }} {{ payment.notes }}
{% trans "Date" %} {% trans "Customer" %} {% trans "Total" %}{% trans "Paid" %}{% trans "Balance" %}{% trans "User" %} {% trans "Status" %} {% trans "Actions" %}
{{ sale.created_at|date:"Y-m-d" }} {{ sale.customer.name|default:_("Guest") }} {{ site_settings.currency_symbol }}{{ sale.total_amount|floatformat:3 }}{{ site_settings.currency_symbol }}{{ sale.paid_amount|floatformat:3 }} - {% if sale.balance_due > 0 %} - {{ site_settings.currency_symbol }}{{ sale.balance_due|floatformat:3 }} - {% else %} - 0.000 - {% endif %} + + + {{ sale.created_by.username|default:"System" }} + {% if sale.status == 'paid' %} @@ -103,10 +99,10 @@
- + {% for method in payment_methods %} + + {% endfor %}
@@ -145,7 +141,7 @@
+ Empty

{% trans "No sales invoices found." %}

@@ -161,6 +165,7 @@ + @@ -168,8 +173,15 @@ {% for payment in purchase.payments.all %} - + + {% endfor %} @@ -181,13 +193,13 @@ {% if purchase.notes %} -
+
{% trans "Notes" %} / ملاحظات

{{ purchase.notes }}

{% endif %} -
+

{% trans "Thank you for your business!" %} / شكراً لتعاملكم معنا!

diff --git a/core/templates/core/purchase_returns.html b/core/templates/core/purchase_returns.html index 5376828..5829777 100644 --- a/core/templates/core/purchase_returns.html +++ b/core/templates/core/purchase_returns.html @@ -35,8 +35,8 @@
- + @@ -48,16 +48,12 @@ - + - - + @@ -51,13 +50,10 @@ - - {% empty %} - diff --git a/core/templates/core/quotations.html b/core/templates/core/quotations.html index ce04f89..e8d5a1e 100644 --- a/core/templates/core/quotations.html +++ b/core/templates/core/quotations.html @@ -36,6 +36,7 @@ + @@ -50,6 +51,11 @@ + {% empty %} - diff --git a/core/templates/core/sales_returns.html b/core/templates/core/sales_returns.html index 838249d..38da94d 100644 --- a/core/templates/core/sales_returns.html +++ b/core/templates/core/sales_returns.html @@ -35,8 +35,8 @@ - + @@ -48,16 +48,12 @@ - +
{% trans "Date" %} / التاريخ {% trans "Method" %} / الطريقة {% trans "Amount" %} / المبلغ{% trans "User" %} / المستخدم {% trans "Notes" %} / ملاحظات
{{ payment.payment_date|date:"Y-m-d" }}{{ payment.payment_method }} + {% if payment.payment_method %} + {% if LANGUAGE_CODE == 'ar' %}{{ payment.payment_method.name_ar }}{% else %}{{ payment.payment_method.name_en }}{% endif %} + {% else %} + {{ payment.payment_method_name }} + {% endif %} + {{ settings.currency_symbol }}{{ payment.amount|floatformat:3 }}{{ payment.created_by.username|default:"System" }} {{ payment.notes }}
{% trans "Return #" %} {% trans "Date" %} {% trans "Supplier" %}{% trans "Original Purchase" %} {% trans "Total Amount" %}{% trans "User" %} {% trans "Actions" %}
{{ return.created_at|date:"Y-m-d" }} {{ return.supplier.name|default:"N/A" }} - {% if return.purchase %} - - #{{ return.purchase.invoice_number|default:return.purchase.id }} - - {% else %} - N/A - {% endif %} - {{ site_settings.currency_symbol }}{{ return.total_amount|floatformat:3 }} + + {{ return.created_by.username|default:"System" }} + + {% trans "Date" %} {% trans "Supplier" %} {% trans "Total" %}{% trans "Paid" %}{% trans "Balance" %}{% trans "User" %} {% trans "Status" %} {% trans "Actions" %}
{{ purchase.created_at|date:"Y-m-d" }} {{ purchase.supplier.name|default:"-" }} {{ site_settings.currency_symbol }}{{ purchase.total_amount|floatformat:3 }}{{ site_settings.currency_symbol }}{{ purchase.paid_amount|floatformat:3 }} - {% if purchase.balance_due > 0 %} - {{ site_settings.currency_symbol }}{{ purchase.balance_due|floatformat:3 }} - {% else %} - 0.000 - {% endif %} + + + {{ purchase.created_by.username|default:"System" }} + {% if purchase.status == 'paid' %} @@ -103,10 +99,10 @@
- + {% for method in payment_methods %} + + {% endfor %}
@@ -145,7 +141,7 @@
+ Empty

{% trans "No purchases recorded yet." %}

{% trans "Date" %} {% trans "Customer" %} {% trans "Total" %}{% trans "User" %} {% trans "Status" %} {% trans "Valid Until" %} {% trans "Actions" %}{{ q.created_at|date:"Y-m-d" }} {{ q.customer.name|default:_("Guest") }} {{ site_settings.currency_symbol }}{{ q.total_amount|floatformat:3 }} + + {{ q.created_by.username|default:"System" }} + + {% if q.status == 'draft' %} {% trans "Draft" %} @@ -120,7 +126,7 @@
+ Empty

{% trans "No quotations found." %}

{% trans "Return #" %} {% trans "Date" %} {% trans "Customer" %}{% trans "Original Sale" %} {% trans "Total Amount" %}{% trans "User" %} {% trans "Actions" %}
{{ return.created_at|date:"Y-m-d" }} {{ return.customer.name|default:_("Guest") }} - {% if return.sale %} - - #{{ return.sale.invoice_number|default:return.sale.id }} - - {% else %} - N/A - {% endif %} - {{ site_settings.currency_symbol }}{{ return.total_amount|floatformat:3 }} + + {{ return.created_by.username|default:"System" }} + + {% endif %} -
-
-
-
-
{% trans "Business Profile" %}
-
-
-
- {% csrf_token %} -
-
- - {% if settings.logo %} - Logo - {% else %} -
- -
- {% endif %} - -
+ -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- -
{% trans "Financial Preferences" %}
-
- - -
{% trans "e.g., OMR, $, £, SAR" %}
-
-
- - -
+
+ +
+ - -
-
-
-
{% trans "Help & Support" %}
+ + +
+ - -
-
- -
{% trans "Smart Admin Version" %}
-

v2.1.0-Meezan

+
+
+ + + + + + + + + + + {% for pm in payment_methods %} + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Name (EN)" %}{% trans "Name (AR)" %}{% trans "Status" %}{% trans "Actions" %}
{{ pm.name_en }}{{ pm.name_ar }} + {% if pm.is_active %} + {% trans "Active" %} + {% else %} + {% trans "Inactive" %} + {% endif %} + + + +
+
+ + {% trans "No payment methods found." %} +
+
+
+ + + + +{% endblock %} + +{% block extra_js %} + {% endblock %} \ No newline at end of file diff --git a/core/templates/core/users.html b/core/templates/core/users.html new file mode 100644 index 0000000..91e568b --- /dev/null +++ b/core/templates/core/users.html @@ -0,0 +1,119 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "User Management" %} - {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
+

{% trans "User Management" %}

+ +
+ +
+
+
+ + + + + + + + + + + + + {% for u in users %} + + + + + + + + + {% endfor %} + +
{% trans "Username" %}{% trans "Email" %}{% trans "Role/Group" %}{% trans "Status" %}{% trans "Last Login" %}{% trans "Actions" %}
+
+
+ +
+
+
{{ u.username }}
+ {% if u.is_superuser %}Superuser{% endif %} +
+
+
{{ u.email|default:"-" }} + {% for group in u.groups.all %} + {{ group.name }} + {% empty %} + No Role + {% endfor %} + + {% if u.is_active %} + {% trans "Active" %} + {% else %} + {% trans "Inactive" %} + {% endif %} + {{ u.last_login|date:"Y-m-d H:i"|default:"Never" }} +
+ {% csrf_token %} + + + +
+
+
+
+
+ + + +{% endblock %} diff --git a/core/templates/registration/login.html b/core/templates/registration/login.html new file mode 100644 index 0000000..52b45f2 --- /dev/null +++ b/core/templates/registration/login.html @@ -0,0 +1,56 @@ +{% extends 'base.html' %} +{% load static i18n %} + +{% block title %}{% trans "Login" %} - {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
+
+
+

{% trans "Welcome Back" %}

+

{% trans "Please login to access your dashboard" %}

+
+ + {% if form.errors %} + + {% endif %} + +
+ {% csrf_token %} +
+ +
+ + +
+
+
+ +
+ + +
+
+ + +
+ +
+

{% trans "Need help? Contact your administrator." %}

+
+
+
+ + +{% endblock %} diff --git a/core/urls.py b/core/urls.py index 4c768d2..2770656 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('users/', views.user_management, name='user_management'), # Invoices (Sales) path('invoices/', views.invoice_list, name='invoices'), @@ -80,4 +81,9 @@ urlpatterns = [ path('inventory/unit/edit//', views.edit_unit, name='edit_unit'), path('inventory/unit/delete//', views.delete_unit, name='delete_unit'), path('api/add-unit-ajax/', views.add_unit_ajax, name='add_unit_ajax'), -] \ No newline at end of file + + # Payment Methods + path('settings/payment-methods/add/', views.add_payment_method, name='add_payment_method'), + 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'), +] diff --git a/core/views.py b/core/views.py index 1617e4c..bf82a9d 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,4 @@ +from django.urls import reverse import random import string from django.shortcuts import render, get_object_or_404, redirect @@ -5,12 +6,14 @@ from django.db.models import Sum, Count, F from django.db.models.functions import TruncDate, TruncMonth from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt +from django.contrib.auth.decorators import login_required from .models import ( Product, Sale, Category, Unit, Customer, Supplier, Purchase, PurchaseItem, PurchasePayment, SaleItem, SalePayment, SystemSetting, Quotation, QuotationItem, - SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem + SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem, + PaymentMethod ) import json from datetime import timedelta @@ -19,6 +22,7 @@ from django.contrib import messages from django.utils.text import slugify import openpyxl +@login_required def index(request): """ Enhanced Meezan Dashboard View @@ -33,7 +37,7 @@ def index(request): low_stock_products = Product.objects.filter(stock_quantity__lt=5) # Recent Transactions - recent_sales = Sale.objects.order_by('-created_at')[:5] + recent_sales = Sale.objects.order_by('-created_at').select_related('created_by')[:5] # Chart Data: Sales for the last 7 days seven_days_ago = timezone.now().date() - timedelta(days=6) @@ -65,31 +69,45 @@ def index(request): } return render(request, 'core/index.html', context) +@login_required def inventory(request): products = Product.objects.all().select_related('category', 'unit', 'supplier') categories = Category.objects.all() - units = Unit.objects.all() suppliers = Supplier.objects.all() context = { 'products': products, 'categories': categories, - 'units': units, 'suppliers': suppliers } return render(request, 'core/inventory.html', context) +@login_required def pos(request): products = Product.objects.all().filter(stock_quantity__gt=0, is_active=True) customers = Customer.objects.all() categories = Category.objects.all() - context = {'products': products, 'customers': customers, 'categories': categories} + payment_methods = PaymentMethod.objects.filter(is_active=True) + + # Ensure at least Cash exists + if not payment_methods.exists(): + PaymentMethod.objects.create(name_en="Cash", name_ar="نقدي", is_active=True) + payment_methods = PaymentMethod.objects.filter(is_active=True) + + context = { + 'products': products, + 'customers': customers, + 'categories': categories, + 'payment_methods': payment_methods + } return render(request, 'core/pos.html', context) +@login_required def customers(request): customers_list = Customer.objects.all().annotate(total_sales=Sum('sales__total_amount')) context = {'customers': customers_list} return render(request, 'core/customers.html', context) +@login_required def suppliers(request): suppliers_list = Supplier.objects.all() context = {'suppliers': suppliers_list} @@ -97,23 +115,37 @@ def suppliers(request): # --- Purchase Views --- +@login_required def purchases(request): - purchases_list = Purchase.objects.all().select_related('supplier').order_by('-created_at') + purchases_list = Purchase.objects.all().select_related('supplier', 'created_by').order_by('-created_at') suppliers_list = Supplier.objects.all() - context = {'purchases': purchases_list, 'suppliers': suppliers_list} + payment_methods = PaymentMethod.objects.filter(is_active=True) + context = { + 'purchases': purchases_list, + 'suppliers': suppliers_list, + 'payment_methods': payment_methods + } return render(request, 'core/purchases.html', context) +@login_required def purchase_create(request): products = Product.objects.filter(is_active=True) suppliers = Supplier.objects.all() - return render(request, 'core/purchase_create.html', {'products': products, 'suppliers': suppliers}) + payment_methods = PaymentMethod.objects.filter(is_active=True) + return render(request, 'core/purchase_create.html', { + 'products': products, + 'suppliers': suppliers, + 'payment_methods': payment_methods + }) +@login_required def purchase_detail(request, pk): purchase = get_object_or_404(Purchase, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/purchase_detail.html', {'purchase': purchase, 'settings': settings}) @csrf_exempt +@login_required def create_purchase_api(request): if request.method == 'POST': try: @@ -124,6 +156,7 @@ def create_purchase_api(request): total_amount = data.get('total_amount', 0) paid_amount = data.get('paid_amount', 0) payment_type = data.get('payment_type', 'cash') + payment_method_id = data.get('payment_method_id') due_date = data.get('due_date') notes = data.get('notes', '') @@ -139,7 +172,8 @@ def create_purchase_api(request): balance_due=float(total_amount) - float(paid_amount), payment_type=payment_type, due_date=due_date if due_date else None, - notes=notes + notes=notes, + created_by=request.user ) # Set status based on payments @@ -153,11 +187,17 @@ def create_purchase_api(request): # Record the initial payment if any if float(paid_amount) > 0: + pm = None + if payment_method_id: + pm = PaymentMethod.objects.filter(id=payment_method_id).first() + PurchasePayment.objects.create( purchase=purchase, amount=paid_amount, - payment_method=payment_type.capitalize(), - notes=_("Initial payment") + payment_method=pm, + payment_method_name=pm.name_en if pm else payment_type.capitalize(), + notes="Initial payment", + created_by=request.user ) for item in items: @@ -179,25 +219,33 @@ def create_purchase_api(request): return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) +@login_required def add_purchase_payment(request, pk): purchase = get_object_or_404(Purchase, pk=pk) if request.method == 'POST': amount = request.POST.get('amount') payment_date = request.POST.get('payment_date', timezone.now().date()) - payment_method = request.POST.get('payment_method', 'Cash') + payment_method_id = request.POST.get('payment_method_id') notes = request.POST.get('notes', '') + pm = None + if payment_method_id: + pm = PaymentMethod.objects.filter(id=payment_method_id).first() + PurchasePayment.objects.create( purchase=purchase, amount=amount, payment_date=payment_date, - payment_method=payment_method, - notes=notes + payment_method=pm, + payment_method_name=pm.name_en if pm else "Cash", + notes=notes, + created_by=request.user ) purchase.update_balance() - messages.success(request, _("Payment added successfully!")) + messages.success(request, "Payment added successfully!") return redirect('purchases') +@login_required def delete_purchase(request, pk): purchase = get_object_or_404(Purchase, pk=pk) for item in purchase.items.all(): @@ -205,27 +253,41 @@ def delete_purchase(request, pk): item.product.save() purchase.delete() - messages.success(request, _("Purchase deleted successfully!")) + messages.success(request, "Purchase deleted successfully!") return redirect('purchases') # --- Sale Views --- +@login_required def invoice_list(request): - sales = Sale.objects.all().order_by('-created_at') + sales = Sale.objects.all().select_related('customer', 'created_by').order_by('-created_at') customers = Customer.objects.all() - return render(request, 'core/invoices.html', {'sales': sales, 'customers': customers}) + payment_methods = PaymentMethod.objects.filter(is_active=True) + return render(request, 'core/invoices.html', { + 'sales': sales, + 'customers': customers, + 'payment_methods': payment_methods + }) +@login_required def invoice_create(request): products = Product.objects.filter(is_active=True) customers = Customer.objects.all() - return render(request, 'core/invoice_create.html', {'products': products, 'customers': customers}) + payment_methods = PaymentMethod.objects.filter(is_active=True) + return render(request, 'core/invoice_create.html', { + 'products': products, + 'customers': customers, + 'payment_methods': payment_methods + }) +@login_required def invoice_detail(request, pk): sale = get_object_or_404(Sale, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/invoice_detail.html', {'sale': sale, 'settings': settings}) @csrf_exempt +@login_required def create_sale_api(request): if request.method == 'POST': try: @@ -237,6 +299,7 @@ def create_sale_api(request): paid_amount = data.get('paid_amount', 0) discount = data.get('discount', 0) payment_type = data.get('payment_type', 'cash') + payment_method_id = data.get('payment_method_id') due_date = data.get('due_date') notes = data.get('notes', '') @@ -253,7 +316,8 @@ def create_sale_api(request): discount=discount, payment_type=payment_type, due_date=due_date if due_date else None, - notes=notes + notes=notes, + created_by=request.user ) # Set status based on payments @@ -267,11 +331,17 @@ def create_sale_api(request): # Record initial payment if any if float(paid_amount) > 0: + pm = None + if payment_method_id: + pm = PaymentMethod.objects.filter(id=payment_method_id).first() + SalePayment.objects.create( sale=sale, amount=paid_amount, - payment_method=payment_type.capitalize(), - notes=_("Initial payment") + payment_method=pm, + payment_method_name=pm.name_en if pm else payment_type.capitalize(), + notes="Initial payment", + created_by=request.user ) for item in items: @@ -325,52 +395,64 @@ def create_sale_api(request): return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) +@login_required def add_sale_payment(request, pk): sale = get_object_or_404(Sale, pk=pk) if request.method == 'POST': amount = request.POST.get('amount') payment_date = request.POST.get('payment_date', timezone.now().date()) - payment_method = request.POST.get('payment_method', 'Cash') + payment_method_id = request.POST.get('payment_method_id') notes = request.POST.get('notes', '') + pm = None + if payment_method_id: + pm = PaymentMethod.objects.filter(id=payment_method_id).first() + SalePayment.objects.create( sale=sale, amount=amount, payment_date=payment_date, - payment_method=payment_method, - notes=notes + payment_method=pm, + payment_method_name=pm.name_en if pm else "Cash", + notes=notes, + created_by=request.user ) sale.update_balance() - messages.success(request, _("Payment added successfully!")) + messages.success(request, "Payment added successfully!") return redirect('invoices') +@login_required def delete_sale(request, pk): sale = get_object_or_404(Sale, pk=pk) for item in sale.items.all(): item.product.stock_quantity += item.quantity item.product.save() sale.delete() - messages.success(request, _("Sale deleted successfully!")) + messages.success(request, "Sale deleted successfully!") return redirect('invoices') # --- Quotation Views --- +@login_required def quotations(request): - quotations_list = Quotation.objects.all().order_by('-created_at') + quotations_list = Quotation.objects.all().select_related('customer', 'created_by').order_by('-created_at') customers = Customer.objects.all() return render(request, 'core/quotations.html', {'quotations': quotations_list, 'customers': customers}) +@login_required def quotation_create(request): products = Product.objects.filter(is_active=True) customers = Customer.objects.all() return render(request, 'core/quotation_create.html', {'products': products, 'customers': customers}) +@login_required def quotation_detail(request, pk): quotation = get_object_or_404(Quotation, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/quotation_detail.html', {'quotation': quotation, 'settings': settings}) @csrf_exempt +@login_required def create_quotation_api(request): if request.method == 'POST': try: @@ -395,7 +477,8 @@ def create_quotation_api(request): discount=discount, valid_until=valid_until if valid_until else None, terms_and_conditions=terms_and_conditions, - notes=notes + notes=notes, + created_by=request.user ) for item in items: @@ -413,10 +496,11 @@ def create_quotation_api(request): return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) +@login_required def convert_quotation_to_invoice(request, pk): quotation = get_object_or_404(Quotation, pk=pk) if quotation.status == 'converted': - messages.warning(request, _("This quotation has already been converted to an invoice.")) + messages.warning(request, "This quotation has already been converted to an invoice.") return redirect('invoices') # Create Sale from Quotation @@ -428,7 +512,8 @@ def convert_quotation_to_invoice(request, pk): balance_due=quotation.total_amount, payment_type='cash', status='unpaid', - notes=quotation.notes + notes=quotation.notes, + created_by=request.user ) # Create SaleItems and Update Stock @@ -448,21 +533,24 @@ def convert_quotation_to_invoice(request, pk): quotation.status = 'converted' quotation.save() - messages.success(request, _("Quotation converted to Invoice successfully!")) + messages.success(request, "Quotation converted to Invoice successfully!") return redirect('invoice_detail', pk=sale.pk) +@login_required def delete_quotation(request, pk): quotation = get_object_or_404(Quotation, pk=pk) quotation.delete() - messages.success(request, _("Quotation deleted successfully!")) + messages.success(request, "Quotation deleted successfully!") return redirect('quotations') # --- Sale Return Views --- +@login_required def sales_returns(request): - returns = SaleReturn.objects.all().order_by('-created_at') + returns = SaleReturn.objects.all().select_related('customer', 'created_by').order_by('-created_at') return render(request, 'core/sales_returns.html', {'returns': returns}) +@login_required def sale_return_create(request): products = Product.objects.filter(is_active=True) customers = Customer.objects.all() @@ -473,12 +561,14 @@ def sale_return_create(request): 'sales': sales }) +@login_required def sale_return_detail(request, pk): sale_return = get_object_or_404(SaleReturn, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/sale_return_detail.html', {'sale_return': sale_return, 'settings': settings}) @csrf_exempt +@login_required def create_sale_return_api(request): if request.method == 'POST': try: @@ -503,7 +593,8 @@ def create_sale_return_api(request): customer=customer, return_number=return_number, total_amount=total_amount, - notes=notes + notes=notes, + created_by=request.user ) for item in items: @@ -524,22 +615,25 @@ def create_sale_return_api(request): return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) +@login_required def delete_sale_return(request, pk): sale_return = get_object_or_404(SaleReturn, pk=pk) for item in sale_return.items.all(): item.product.stock_quantity -= item.quantity item.product.save() sale_return.delete() - messages.success(request, _("Sale return deleted successfully!")) + messages.success(request, "Sale return deleted successfully!") return redirect('sales_returns') # --- Purchase Return Views --- +@login_required def purchase_returns(request): - returns = PurchaseReturn.objects.all().order_by('-created_at') + returns = PurchaseReturn.objects.all().select_related('supplier', 'created_by').order_by('-created_at') return render(request, 'core/purchase_returns.html', {'returns': returns}) +@login_required def purchase_return_create(request): products = Product.objects.filter(is_active=True) suppliers = Supplier.objects.all() @@ -550,12 +644,14 @@ def purchase_return_create(request): 'purchases': purchases }) +@login_required def purchase_return_detail(request, pk): purchase_return = get_object_or_404(PurchaseReturn, pk=pk) settings = SystemSetting.objects.first() return render(request, 'core/purchase_return_detail.html', {'purchase_return': purchase_return, 'settings': settings}) @csrf_exempt +@login_required def create_purchase_return_api(request): if request.method == 'POST': try: @@ -580,7 +676,8 @@ def create_purchase_return_api(request): supplier=supplier, return_number=return_number, total_amount=total_amount, - notes=notes + notes=notes, + created_by=request.user ) for item in items: @@ -601,17 +698,19 @@ def create_purchase_return_api(request): return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) +@login_required def delete_purchase_return(request, pk): purchase_return = get_object_or_404(PurchaseReturn, pk=pk) for item in purchase_return.items.all(): item.product.stock_quantity += item.quantity item.product.save() purchase_return.delete() - messages.success(request, _("Purchase return deleted successfully!")) + messages.success(request, "Purchase return deleted successfully!") return redirect('purchase_returns') # --- Other Management Views --- +@login_required def reports(request): """ Smart Reports View @@ -633,6 +732,7 @@ def reports(request): } return render(request, 'core/reports.html', context) +@login_required def settings_view(request): """ Smart Admin Settings View @@ -658,8 +758,42 @@ def settings_view(request): messages.success(request, "Settings updated successfully!") return redirect('settings') - return render(request, 'core/settings.html', {'settings': settings}) + payment_methods = PaymentMethod.objects.all() + + return render(request, 'core/settings.html', { + 'settings': settings, + 'payment_methods': payment_methods, + }) +@login_required +def add_payment_method(request): + if request.method == 'POST': + name_en = request.POST.get('name_en') + name_ar = request.POST.get('name_ar') + is_active = request.POST.get('is_active') == 'on' + PaymentMethod.objects.create(name_en=name_en, name_ar=name_ar, is_active=is_active) + messages.success(request, "Payment method added successfully!") + return redirect('settings') + +@login_required +def edit_payment_method(request, pk): + pm = get_object_or_404(PaymentMethod, pk=pk) + if request.method == 'POST': + pm.name_en = request.POST.get('name_en') + pm.name_ar = request.POST.get('name_ar') + pm.is_active = request.POST.get('is_active') == 'on' + pm.save() + messages.success(request, "Payment method updated successfully!") + return redirect('settings') + +@login_required +def delete_payment_method(request, pk): + pm = get_object_or_404(PaymentMethod, pk=pk) + pm.delete() + messages.success(request, "Payment method deleted successfully!") + return redirect('settings') + +@login_required def add_customer(request): if request.method == 'POST': name = request.POST.get('name') @@ -670,6 +804,7 @@ def add_customer(request): messages.success(request, "Customer added successfully!") return redirect('customers') +@login_required def edit_customer(request, pk): customer = get_object_or_404(Customer, pk=pk) if request.method == 'POST': @@ -681,12 +816,14 @@ def edit_customer(request, pk): messages.success(request, "Customer updated successfully!") return redirect('customers') +@login_required def delete_customer(request, pk): customer = get_object_or_404(Customer, pk=pk) customer.delete() messages.success(request, "Customer deleted successfully!") return redirect('customers') +@login_required def add_supplier(request): if request.method == 'POST': name = request.POST.get('name') @@ -696,6 +833,7 @@ def add_supplier(request): messages.success(request, "Supplier added successfully!") return redirect('suppliers') +@login_required def edit_supplier(request, pk): supplier = get_object_or_404(Supplier, pk=pk) if request.method == 'POST': @@ -706,6 +844,7 @@ def edit_supplier(request, pk): messages.success(request, "Supplier updated successfully!") return redirect('suppliers') +@login_required def delete_supplier(request, pk): supplier = get_object_or_404(Supplier, pk=pk) supplier.delete() @@ -713,6 +852,7 @@ def delete_supplier(request, pk): return redirect('suppliers') +@login_required def suggest_sku(request): """ API endpoint to suggest a unique SKU. @@ -723,6 +863,7 @@ def suggest_sku(request): if not Product.objects.filter(sku=sku).exists(): return JsonResponse({"sku": sku}) +@login_required def add_product(request): if request.method == 'POST': name_en = request.POST.get('name_en') @@ -767,8 +908,9 @@ def add_product(request): product.save() messages.success(request, "Product added successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#items') +@login_required def edit_product(request, pk): product = get_object_or_404(Product, pk=pk) if request.method == 'POST': @@ -795,15 +937,17 @@ def edit_product(request, pk): product.save() messages.success(request, "Product updated successfully!") - return redirect('inventory') - return redirect('inventory') + return redirect(reverse('inventory') + '#items') + return redirect(reverse('inventory') + '#items') +@login_required def delete_product(request, pk): product = get_object_or_404(Product, pk=pk) product.delete() messages.success(request, "Product deleted successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#items') +@login_required def add_category(request): if request.method == 'POST': name_en = request.POST.get('name_en') @@ -811,8 +955,9 @@ def add_category(request): slug = slugify(name_en) Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug) messages.success(request, "Category added successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#categories-list') +@login_required def edit_category(request, pk): category = get_object_or_404(Category, pk=pk) if request.method == 'POST': @@ -821,14 +966,16 @@ def edit_category(request, pk): category.slug = slugify(category.name_en) category.save() messages.success(request, "Category updated successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#categories-list') +@login_required def delete_category(request, pk): category = get_object_or_404(Category, pk=pk) category.delete() messages.success(request, "Category deleted successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#categories-list') +@login_required def add_unit(request): if request.method == 'POST': name_en = request.POST.get('name_en') @@ -836,8 +983,9 @@ def add_unit(request): short_name = request.POST.get('short_name') Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name) messages.success(request, "Unit added successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#units-list') +@login_required def edit_unit(request, pk): unit = get_object_or_404(Unit, pk=pk) if request.method == 'POST': @@ -846,19 +994,22 @@ def edit_unit(request, pk): unit.short_name = request.POST.get('short_name') unit.save() messages.success(request, "Unit updated successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#units-list') +@login_required def delete_unit(request, pk): unit = get_object_or_404(Unit, pk=pk) unit.delete() messages.success(request, "Unit deleted successfully!") - return redirect('inventory') + return redirect(reverse('inventory') + '#units-list') +@login_required def barcode_labels(request): products = Product.objects.filter(is_active=True).order_by('name_en') context = {'products': products} return render(request, 'core/barcode_labels.html', context) +@login_required def import_products(request): """ Import products from an Excel (.xlsx) file. @@ -869,7 +1020,7 @@ def import_products(request): if not excel_file.name.endswith('.xlsx'): messages.error(request, "Please upload a valid .xlsx file.") - return redirect('inventory') + return redirect(reverse('inventory') + '#items') try: wb = openpyxl.load_workbook(excel_file) @@ -938,9 +1089,10 @@ def import_products(request): except Exception as e: messages.error(request, f"Error processing file: {str(e)}") - return redirect('inventory') + return redirect(reverse('inventory') + '#items') @csrf_exempt +@login_required def add_category_ajax(request): if request.method == 'POST': try: @@ -963,6 +1115,7 @@ def add_category_ajax(request): return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) @csrf_exempt +@login_required def add_unit_ajax(request): if request.method == 'POST': try: @@ -985,6 +1138,7 @@ def add_unit_ajax(request): return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) @csrf_exempt +@login_required def add_supplier_ajax(request): if request.method == 'POST': try: @@ -1004,3 +1158,46 @@ def add_supplier_ajax(request): except Exception as e: return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) + +@login_required +def user_management(request): + if not (request.user.is_superuser or request.user.groups.filter(name='admin').exists()): + messages.error(request, "Access denied.") + return redirect('index') + + from django.contrib.auth.models import User, Group + users = User.objects.all().prefetch_related('groups') + groups = Group.objects.all() + + if request.method == 'POST': + action = request.POST.get('action') + if action == 'add': + username = request.POST.get('username') + password = request.POST.get('password') + email = request.POST.get('email') + group_id = request.POST.get('group') + + if User.objects.filter(username=username).exists(): + messages.error(request, "Username already exists.") + else: + user = User.objects.create_user(username=username, email=email, password=password) + if group_id: + group = Group.objects.get(id=group_id) + user.groups.add(group) + user.is_staff = True + user.save() + messages.success(request, f"User {username} created successfully.") + + elif action == 'toggle_status': + user_id = request.POST.get('user_id') + user = get_object_or_404(User, id=user_id) + if user == request.user: + messages.error(request, "You cannot deactivate yourself.") + else: + user.is_active = not user.is_active + user.save() + messages.success(request, f"User {user.username} status updated.") + + return redirect('user_management') + + return render(request, 'core/users.html', {'users': users, 'groups': groups}) \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css index a3ebcdc..d5cb873 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -101,6 +101,43 @@ body { font-size: 1.2rem; } +/* Collapsible Sidebar Styles */ +#sidebar ul.components li.sidebar-group-header > a { + padding: 12px 25px; + font-size: 0.85rem; + text-transform: uppercase; + font-weight: 700; + color: #6c757d; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 0; + margin-inline-end: 0; +} + +#sidebar ul.components li.sidebar-group-header > a:hover { + background: transparent; + color: var(--meezan-primary); +} + +#sidebar ul.components li.sidebar-group-header > a i.chevron { + transition: transform 0.3s; +} + +#sidebar ul.components li.sidebar-group-header > a[aria-expanded="true"] i.chevron { + transform: rotate(180deg); +} + +#sidebar ul.sub-menu li a { + padding-inline-start: 50px; + font-size: 0.95rem; +} + +[dir="rtl"] #sidebar ul.sub-menu li a { + padding-inline-end: 50px; + padding-inline-start: 25px; +} + /* Main Content Styling */ #content { width: 100%; @@ -156,4 +193,4 @@ body { #content { width: 100%; } -} +} \ No newline at end of file diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index a3ebcdc..d5cb873 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -101,6 +101,43 @@ body { font-size: 1.2rem; } +/* Collapsible Sidebar Styles */ +#sidebar ul.components li.sidebar-group-header > a { + padding: 12px 25px; + font-size: 0.85rem; + text-transform: uppercase; + font-weight: 700; + color: #6c757d; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 0; + margin-inline-end: 0; +} + +#sidebar ul.components li.sidebar-group-header > a:hover { + background: transparent; + color: var(--meezan-primary); +} + +#sidebar ul.components li.sidebar-group-header > a i.chevron { + transition: transform 0.3s; +} + +#sidebar ul.components li.sidebar-group-header > a[aria-expanded="true"] i.chevron { + transform: rotate(180deg); +} + +#sidebar ul.sub-menu li a { + padding-inline-start: 50px; + font-size: 0.95rem; +} + +[dir="rtl"] #sidebar ul.sub-menu li a { + padding-inline-end: 50px; + padding-inline-start: 25px; +} + /* Main Content Styling */ #content { width: 100%; @@ -156,4 +193,4 @@ body { #content { width: 100%; } -} +} \ No newline at end of file