From 60ef14cac462b397553fa684353007d36afcaf4a Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 4 Feb 2026 16:31:58 +0000 Subject: [PATCH] Ver 10.3 fixing Spark --- config/__pycache__/settings.cpython-311.pyc | Bin 5676 -> 5822 bytes config/__pycache__/urls.cpython-311.pyc | Bin 1660 -> 1766 bytes config/settings.py | 12 ++- config/urls.py | 3 +- core/__pycache__/admin.cpython-311.pyc | Bin 2197 -> 2276 bytes core/__pycache__/models.cpython-311.pyc | Bin 11176 -> 12056 bytes core/__pycache__/views.cpython-311.pyc | Bin 37715 -> 37639 bytes core/admin.py | 5 +- ..._of_employment_worker_id_photo_and_more.py | 34 +++++++++ ...t_worker_id_photo_and_more.cpython-311.pyc | Bin 0 -> 1510 bytes core/models.py | 12 +++ core/templates/core/email/payslip_email.html | 70 ++++++++++++++++++ core/views.py | 32 ++++---- requirements.txt | 1 + 14 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 core/migrations/0009_worker_date_of_employment_worker_id_photo_and_more.py create mode 100644 core/migrations/__pycache__/0009_worker_date_of_employment_worker_id_photo_and_more.cpython-311.pyc create mode 100644 core/templates/core/email/payslip_email.html diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 99b006336d94e33e919c29838ee5789ac1161442..80f84c6fc461051382555cda8a443bc2c7a17eeb 100644 GIT binary patch delta 1220 zcmah{%TE(w5dXeDZDISM4+@kPg#v{>c$BwQiv_6>2@ezV#e@*2tyP4k?ZGt0Hjx-l z#%3=ha!?O=zzeAdPbLx*FUCmXf%Gr%xF&{pbasK%E+&4-{ATtyznz_#Z{PYp_ziD3 zPEYX6-+G+7X)75V^l-P!Obbx2AaUhgDJSNE1H-n0kR7z6lT7zK zl~6>UI~2?`co8_+X4aRgkXWS;;1e(H}tW#xDNXBb%e097!8OK`oUu# z5`9w1&cP6CM}AmRcF0;9fzjHQm`A1<`)jCzbS114C0#PvI84+Q+NOAgXh(;2%k<}~ z`#n~8Ws2}AMUP1)xllR`TY6>WMYvS@JrVQC=m<>KHpu*N`PAPUjr(g=F9ZBXtwl*- zivOx|yMu9GMgh~T7p|~Am|^`8V*}_1LAgl0SIhP47c`Q$Ce|OUt|tSD)s-Cj)a}4Z zI$hDt%q=c0PtQlMS4iG@CznfS$AiItN%?o0(nuTbB^y8}5`ic+<|;h53mT#~30h=} zi7GEe{3GpW>M2j3d8jt!wZZ#wNUa1bYRQ-PxICr1eAxJmhRR=zdvvy|b26=6(-aSUKe?aVP}U$$SSqXYsT+HPR?Q o#j$@$v#U|{jaUUkdB*aJnmBt=cfi>;wfmgAs24QlAJ!=S1BEK`>i_@% delta 944 zcmZWnOKVd>6rQ>FrZs79-tRU^+a!&7HhniqqqU7iHxa2DXXECC5Gx`{S5;`hE-FHy z3_`&!v_f6fjb!7h3qi$Q1s5f&{)Ag_B_f`iHrU(_obNkx=3~w`kHgf5r0KmZ8wie< zh3C~Tj%|~No_vg2X-*(z@nL&KT?He^`vM^c7-yp*G833K$YmW*3x3fIGT)(t5;KE^ zA5!;yE3<%=S;59^81@PQ`yOQuWCyYnoXiC-<_0%btl&ZP?$Ig*->y`l%me+*3j^TS zv2iqK^23`_^>!EJT5QYfuII`PYrOXFWxQeqV#M&5dW1>nc66=Q~8-Nt^ zqvHTZ5YlY0tcTIHK0??KW*Id@9y{l8mGMf3FZwIty-mPmw+R-yC_%*(7&MA7Dk2)y zG|cc#(VrjD)MvHt!lD{Qwxi&?qL=3QZ?RyHX*7A5*B>{{kud%w|8 zbL+}8UDIb;qx68kGH=oGX4vwXPS+-I%i1JDORPR#txc)Z85L$#$f}T2VGg0Ct3P>E zU9HXY30t{u1uN<~>!%`rXZtiHHx@Q-Z?5c&|BwPdrGU~aS%rqI7&~rG_zQc)jv*_P4b8)VWCi0kHbFWB>pF diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index ecbd4a1807173bf6d33227761d7c642acae555cf..52c9fc585d599622fbec0bf5f90a97410a378157 100644 GIT binary patch delta 159 zcmeyv^Ng2wIWI340}%8DH)lSc$ScX1Hc|Zqqu<02DU%f#Ie7&^av+e(o+Y%|jM0vn zQFwAG%L<`eoW8Cuo{sUMK|Z&*z|6@KtnOSgK&6a8T&y)YlXVv}Gh^UnH8w|+8!Y@2 u%qE!4(6}O{v%}+xjsFGVfQ!NbSAZl-K!Z03eqaYGX8gbaB8#MeMgRaV8!GGo delta 91 zcmaFH`-g{jIWI340}wRXHfK(p$ScWcG*SHoqshb%DVsYO6_^-u zstvw3#EcvKL2&XP=4D)pAj3ce-{dtcnv9B*ud#&aD*^c{8E>(J?Jg?zD^dZe5(W_> zAVL&GsKW^1$&sv@jOvq{StEHgK!Tb;0^%sq$$wdSOy18X%V;wBF541QW=4|_44A|O)hj|uA3+jdK*UF81}2FsjN%`dKuWnL IZ)3j?0HQZ%6#xJL delta 318 zcmaDNI8~5uIWI340}!N0H)WPHZ{#yzo@~b|%f}s_UzD9%J=fSk06OJAmSr41CzuRM)40!Af+6WSvl?l0DQGXHvj+t diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 1255e6c72fd43e063fbeedd9575b495e25a7152e..d8999040e5a95506751d968068aa40e012327bfa 100644 GIT binary patch delta 2005 zcmZ`(UrbYH6u)07?)j953*G6?aDfXBO4B zNT&Wn@o`GkbCjwVoK$nh=I0nMhVg5XT75xSWR#@>O&AC;?dF%XTU4k2f-vx!D)kG| z04QbG1}DTZgPzr>*)lR@qz$trT4a@FMv_KMRni7+WG*eco=GMrTm@o6HCrZ}J)Lr= z5znMFJsq{vF2>_zx*w%h1S}1WA~eC@Wpg9~bLHFNo~JA{$&{Z99}C0mH^1ipt(zb~ zQ+dEPZ-e{68rysk{!FoSZ4>QBv~-5*rkozqWIdBQtx?$+l+}cpPU!KpJVK3OEuKbu z!j$vb%0saYQI!q7*(%u}MeSA@GL)oo#@HTd`Nesmdzy2v$p? zkZ^4Y2@dzV%~sq&)&<1JMNDzwgAG+FoDzfC8?FXY%qI@+SDYn#V7L3Ss9>@PJJ>eA zfXR|tcUA3#1KHp0nP%3BvV%rUZ#HA2+5mjyUvToseBj0~MYoIF z6~#gat{u9z1cL%?_b%GCLQIj9yh4V~5?YZt;a{3ZngGfZTUP7wC;H%G_O zgm46^0)LS4?9)K0jl{tb93|)Blc1k`23LcZhz{FAVFwR9%38UTm@pF>>VH*=yfoM> z%RLesr$#cVP%UmymB+E{?DMD7XZJXP89Y~gXbnfoXHki8Iy(~HO2{nl>ylh=BMY!r zv)_3G30Dy2p|1A4M`vQx#y&Qht{RjYaHn=VxeSkLm1dr!1F5{_K9pVziaHr#2~u?t zaulxCb(LR71;S|d_quLEuCpY5A^?BYZ)MlN!M$VIEScqH7fK6vy{DW*ysjvrZgh$90?y;Mk7&C4E)8D#wRsVeC3Ii zm#D2EJMR3rW#RVVD6GIAf>Sl!v>_qMN8wAc@<{Db=)PSSfJVoAz-FUa%idpLJ9t5F zs){Y76}_m1Ot0e^WA=zSSj78U6lX#zti!vZ_qI9SEqc-H7&a|~=~UiNND$(Ly@WM{ z^@KixO!$!S5n(llvmkF5%>w?|Qt#VILBcBBU4IpJ;@7E2xoJ@v76@Oc_%+No^n#8L zv93vi1D(f?hJW;h)hfej)rW&#)Xh zR2GVSHd`pNY_f6kdjQ_)+|DVdpi!OpW53a=xSa7V1M@gRRK z&=uXp!W6aV(19mne}G-iHC4Ny2TRQ*_yk`xhv68?+9d43^IBXM!lU@Twifo|OKqs< zZ5Bmm=-?b>Oh3qM%gQoy&}N1ZbKDkh1bg!x^FIM^wj_NMWQ)*Oe%7J^Oo-!+E=joH zE8Lve>c2<@pA#Q{35+MnB_^XO1It^A1qdm68}wy;WVmk z8Y(vtHhxfr89xGO)ZqW9XKJdRZ?3<-I;UrGC9fnz~&zy>j-%;E=(N=L>qB&yM lS$vp@!5#cP^C%O5$Y0X`akB5!uD>b%&zodZ`R8Tt!)pTHD@_0Z diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index bed590abd2fa6b3ef13f87716f35dda06e07dd95..e3d27357041fa0b8c371e3d276721c77f85fc23c 100644 GIT binary patch delta 1142 zcmY*WTWlLu5WRQpHSuGeO^V6Ok5HVXPSfNgp~5O@NCSk_X`AMuhKJ&q?8c8ew%Of? zq*~)5kZ@8J+Kv!NDbW5H6)FK*f%vMT@Eew zsqEfY-BM$m=IiZ58!%#RtC7#vbN&kJ85^c;3ASBgc+B%UQ z)wz_<^f!p)fla9+JZ>_C1c}m@7az*+GF6uUn0C_iSX(0aZIBiCgdjC z3<=q{r}^R0rbws-K9s*04OuHTMPA4%QpJ(Z#?|OTb~&R}oHaYPgpSh0Hgd+tV&pz~ zDV9w46H_oItIbI2~hTur{ zAkD*Sc9w==JNv$PjFHN5gxZ*4cs2LdUn2!{sWuE)AVWRSsUP$LB>OF z?IK+SZvDJ?1k>D@_AbH*e6k+2Eg~tyt#$9=6q?T=j3VIED$gUlz(94eoc8}6r5lUH zQAhaAIB@PWYb(C4f_qYMa0Q1{ustOX_uli-f>D3r0@pG@X{bsN?^y^ZzU{AaHA;eS HFKzq>24Wyu delta 1325 zcmZ8fO>7%Q6rS<=FYBzmCCxuh){RrgX__Vih2r3lzovqcT-`GLv}Iy$G7Jr{1*Wb@r#@%Ck2qYIQFuN4%z`e@&7tE96VyfBggjxc^x?uV zgg=xjyw_9|Se;IEkZ*}?0^xveO1O7el|P2_P5XEQ=A?Mlu>PQK#@MJ^FgEEnjLjdo z3iU!u3>KI=pgmjD0-!tedTn%%Cp=){5!0D1GXS69wPO%yCu%+biLRQj1EKYQ4Yr}* zgzkpvngReXP>vOx+6i=9Xm+)0FYIM>Cz=$yyB{w&FFMn9q57YLIgqDb205tN_LPDf zT^HRA?k$MB!fVIQ0)Gd(E#4}wbk6b@)TET*PKs$!;Wbg9={YefD*`t&8Q{8B>gaQ_ zvMeeAZpB7VVgYts^t`O`Np6;xga9|ktu)j0xFYh)f}BeS=y6^Zaqa@wvqEzMuJ5mk zSb9uIWK}ICrZu%1@p@@`J}#*o{_$y!&uVgtw7{ihO1v;Dx(&z(oh zodX7iT?1g)Zuh)DY*=d4u!6Rpa%-Pq+l|=4p@Fhz$f(8qFNGJcoI_w4C{lvd)s!X%y5BInH4B3YpaJ08i>z={@Z-1<#`PyceE9 zqxsjc|3$vX@*$mRQ*b!1FNcK2Kym{gcqd;*1o<^I+=NtE;VUn-j z@-X_h=(D_n`K?mVr3E{Dh}Kq@;nV2v)hAg$tklP~R5FqhRh5s4sDJG(xQOnoU1Ni! zf|KlUoxlisb=^C)NP;|u3Z0bFVnou!lv<(HZ0gbLQDQ_fMqr#klxUeul9$r8J(LOb z^?DmEkTikq(y!|un~#$>q>!O0WJU^^fI_;iYLqPLp^rc>hJWnOq+}tR6i1Xo>6 diff --git a/core/admin.py b/core/admin.py index deee1d0..e82cf4b 100644 --- a/core/admin.py +++ b/core/admin.py @@ -7,8 +7,9 @@ class UserProfileAdmin(admin.ModelAdmin): @admin.register(Worker) class WorkerAdmin(admin.ModelAdmin): - list_display = ('name', 'id_no', 'phone_no', 'monthly_salary') + list_display = ('name', 'id_no', 'phone_no', 'monthly_salary', 'date_of_employment', 'projects_worked_on_count') search_fields = ('name', 'id_no') + readonly_fields = ('projects_worked_on_count',) # Calculated field should be readonly in edit form @admin.register(Project) class ProjectAdmin(admin.ModelAdmin): @@ -24,4 +25,4 @@ class TeamAdmin(admin.ModelAdmin): class WorkLogAdmin(admin.ModelAdmin): list_display = ('date', 'project', 'supervisor') list_filter = ('date', 'project', 'supervisor') - filter_horizontal = ('workers',) + filter_horizontal = ('workers',) \ No newline at end of file diff --git a/core/migrations/0009_worker_date_of_employment_worker_id_photo_and_more.py b/core/migrations/0009_worker_date_of_employment_worker_id_photo_and_more.py new file mode 100644 index 0000000..055d561 --- /dev/null +++ b/core/migrations/0009_worker_date_of_employment_worker_id_photo_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.7 on 2026-02-04 14:11 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_alter_expensereceipt_payment_method_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='worker', + name='date_of_employment', + field=models.DateField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='worker', + name='id_photo', + field=models.ImageField(blank=True, null=True, upload_to='workers/ids/', verbose_name='ID Document'), + ), + migrations.AddField( + model_name='worker', + name='notes', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='worker', + name='photo', + field=models.ImageField(blank=True, null=True, upload_to='workers/photos/'), + ), + ] diff --git a/core/migrations/__pycache__/0009_worker_date_of_employment_worker_id_photo_and_more.cpython-311.pyc b/core/migrations/__pycache__/0009_worker_date_of_employment_worker_id_photo_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..160820860b7db87163ea789f404413aa8b020067 GIT binary patch literal 1510 zcmb7E&2QX96d$iYcI|AMWYh|z1OZK2)vB}(7X(s;K-v_FpqEBO+CxX)otKTbw#W4c zO>>F_2adgQ;u?y&2mS>9gpK6GYOb8PZIzyigqRtx6E%tWnDIQ%^Ly|2zGnRM;-Z7Z z`02rS!Jj%pze}bb=6rL~6`LOsMN|t>PZRG(*ogF=9vMADLk;vfqWbrU8laDA8+0Vl zUWiqVo=LR_$Tk0v?0vVvohTTlejczm<5t8dgxO%Ot6uFnbMl2IeS`AbYfx>d_4Eb; z=Xn{P&Y0pC*ibxeph#cWY~>>L>a zN38#dv!HMmYkk@rxBq7(isgD&T{q8dtHI=juwFt_YnJ3v%CG z;w_>O2;mM9xzQp7v2PRdxbQ<6u;eI+s!)gr0c5E>)4cT=rFUf_scb&CyP^(?=N29L z@sM$|$b&HBtvra}fW^S|m_6any~rQVHSKNK&nqK`B{07Yzn~L=h+dDpJr;pi^ouwz zJlKapk+am3Ic14I08jSLJG(`m=;FtuN|{`2OD(C(e%i}U@-(%CLq+5b%Kj4M@N~{B zWZ%_hdgH2f_|?oo_Sz3l*;<{x_3m`##uQ(FcC*CSpMN;MQ{MixyxpDPtz*1Z;;mWJ zu$`AkkXMqNvs|6xb!oZ&eB;;c-yTl-Y=V50sD5_pcBX4Wh1+u(?W<#<4ntMVb%~&0 zZiwCM)}88XeUjc1to-q0eL-frrfH(W+B*}pRy|X6YjW8$vsN{0hAxt4PPx4CQVL#O IB2_W`4L*a6WdHyG literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 69779cf..c43ef4b 100644 --- a/core/models.py +++ b/core/models.py @@ -27,6 +27,13 @@ class Worker(models.Model): id_no = models.CharField(max_length=50, unique=True, verbose_name="ID Number") phone_no = models.CharField(max_length=20, verbose_name="Phone Number") monthly_salary = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.00'))]) + + # New fields + photo = models.ImageField(upload_to='workers/photos/', blank=True, null=True) + id_photo = models.ImageField(upload_to='workers/ids/', blank=True, null=True, verbose_name="ID Document") + date_of_employment = models.DateField(default=timezone.now) + notes = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) is_active = models.BooleanField(default=True) @@ -34,6 +41,11 @@ class Worker(models.Model): def day_rate(self): return self.monthly_salary / Decimal('20.0') + @property + def projects_worked_on_count(self): + """Returns the number of distinct projects this worker has worked on.""" + return self.work_logs.values('project').distinct().count() + def __str__(self): return self.name diff --git a/core/templates/core/email/payslip_email.html b/core/templates/core/email/payslip_email.html new file mode 100644 index 0000000..48c203f --- /dev/null +++ b/core/templates/core/email/payslip_email.html @@ -0,0 +1,70 @@ + + + + + + +
+
+
Payslip
+
Reference: #{{ record.id }}
+
+ +
+ Worker: {{ record.worker.name }}
+ ID Number: {{ record.worker.id_no }}
+ Date: {{ record.date }} +
+ + + + + + + + + + + + + + + + + {% for adj in adjustments %} + + + + + {% endfor %} + +
DescriptionAmount
Base Pay ({{ logs_count }} days worked)R {{ logs_amount }}
{{ adj.get_type_display }}: {{ adj.description }} + R {{ adj.amount }} +
+ +
+

Net Pay: R {{ record.amount }}

+
+ + +
+ + diff --git a/core/views.py b/core/views.py index 1e77768..90f1af0 100644 --- a/core/views.py +++ b/core/views.py @@ -590,21 +590,27 @@ def process_payment(request, worker_id): # Email Notification subject = f"Payslip for {worker.name} - {payroll_record.date}" - message = ( - f"Payslip Generated\n\n" - f"Record ID: #{payroll_record.id}\n" - f"Worker: {worker.name}\n" - f"Date: {payroll_record.date}\n" - f"Total Paid: R {payroll_record.amount}\n\n" - f"Breakdown:\n" - f"Base Pay ({log_count} days): R {logs_amount}\n" - f"Adjustments: R {adj_amount}\n\n" - f"This is an automated notification." - ) + + # Prepare HTML content + context = { + 'record': payroll_record, + 'logs_count': log_count, + 'logs_amount': logs_amount, + 'adjustments': payroll_record.adjustments.all(), + } + html_message = render_to_string('core/email/payslip_email.html', context) + plain_message = strip_tags(html_message) + recipient_list = ['foxfitt-ed9wc+expense@to.sparkreceipt.com'] try: - send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list) + send_mail( + subject, + plain_message, + settings.DEFAULT_FROM_EMAIL, + recipient_list, + html_message=html_message + ) messages.success(request, f"Payment processed for {worker.name}. Net Pay: R {payroll_record.amount}") except Exception as e: messages.warning(request, f"Payment processed, but email delivery failed: {str(e)}") @@ -801,4 +807,4 @@ def create_receipt(request): return render(request, 'core/create_receipt.html', { 'form': form, 'items': items - }) \ No newline at end of file + }) diff --git a/requirements.txt b/requirements.txt index e22994c..65b2871 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 +Pillow