From 1955ecf8b3bb94c365c930b7326561532d06262f Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 15 Nov 2025 19:14:03 +0000 Subject: [PATCH] 1.4 --- core/__pycache__/decorators.cpython-311.pyc | Bin 0 -> 1307 bytes core/__pycache__/models.cpython-311.pyc | Bin 6351 -> 6989 bytes core/__pycache__/urls.cpython-311.pyc | Bin 1955 -> 2100 bytes core/__pycache__/views.cpython-311.pyc | Bin 11701 -> 13669 bytes core/decorators.py | 12 ++++ core/etenders_api.py | 18 ++++++ core/management/__init__.py | 0 core/management/commands/__init__.py | 0 core/management/commands/import_tenders.py | 52 ++++++++++++++++++ core/migrations/0004_tender_status.py | 18 ++++++ .../0004_tender_status.cpython-311.pyc | Bin 0 -> 1394 bytes core/models.py | 13 +++++ core/templates/core/tender_detail.html | 21 +++++++ core/urls.py | 1 + core/views.py | 28 +++++++++- requirements.txt | 1 + 16 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 core/__pycache__/decorators.cpython-311.pyc create mode 100644 core/decorators.py create mode 100644 core/etenders_api.py create mode 100644 core/management/__init__.py create mode 100644 core/management/commands/__init__.py create mode 100644 core/management/commands/import_tenders.py create mode 100644 core/migrations/0004_tender_status.py create mode 100644 core/migrations/__pycache__/0004_tender_status.cpython-311.pyc diff --git a/core/__pycache__/decorators.cpython-311.pyc b/core/__pycache__/decorators.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa6e44cef04cafe6591a136d454c795201087689 GIT binary patch literal 1307 zcmah}%S#(U7@yf}V$?)!SFQCyw;*i;CMktN3!+e=Eg~ZJAf|=5?vBZ7vb&wxHP)t( zgU|}4rxtsZQf!Mo_?MJb0)~a2dg^V#i+Jic8%+uJ@iF_G*Z2EgGqa!K@iqjq?)K@o z1%!U^$(g`BkfUMfwvdK2TtG#f!I+PQf=~=)LNE?(pp2-Ak5D4~lM@oy3qPQyW5J;3 zp>12UgjuG+$bFM8>Y7Fj6$ggvz}>ay#ViU zKoEc;{GAd8%`2SpjZ=gzR7T4%s>^aC7CPCqewIGk=Fy##&Cpl81KiyPe_K4K1PeRx z?KU=8tjPzDED#zK(56^MImr0~o<%w;3m@?t$ls2i=^!Y2vqz{-Xd0>-0lS<>dCG~f*FwothcMd{6`BLiN-5~u0~=W$ z9rIdObyCVMIflx3+OjNHDkrznd9z5;&Z1-3PMWL{)v-;QE}3+NS&B;1s!2&&BhV?Z z!BW;UFP<$?#j=Q&m+_sMgv0x?JrX z@sSW8uSxxHN2}7cLn(P6CHJ1YQgUCKs7e!u(&T|Oxi3vsr70ib3*+w0Ty^G2bw&?n zwSnRHcem%8@6ejdRU3!DYv)dS^~If;$}$0?98ItWw#4B0&ZKZG%^p%J&a)lkxHZoU}B KJw7@QfsbFD+)ID} literal 0 HcmV?d00001 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 5c123ea6c6b2f350a537946a3845645117931692..dc278a1a91e38270d9cd65eaba854a70e3d06901 100644 GIT binary patch delta 1328 zcmZWpOKcle6rE?gj(=l2{>BxiBZNX7bwX1JDwst1sUZ^i5i1g*X*8V~he_=j^JXRx zBVi*U)I~SZUBD7Hg@!CZSs@`Ix4xk1qL+#CKe!W>gJT zaSY3J&!UUv#~JB)Y1Vqsh6M}nC0{e`c#Fwg-!yE8dsWReIcVuzdgCz`#j)##CO2w| zX&UBcGd5*t(o%PZ*^Hwrb=gtu8?s{C#->@Gu^Z_eUipLGhG2TkPOq&xvA99{kEaMi+$>inxUAgE~{_r>W$_z zb6o}QgcMjmeWH=3NrQb-N~6K8ZKY<*WLvhXRW~QJ1YxqrW8Dg79<+BchZT9-yD_n6~MIXbZn3 z(zlw2p#m&tFTq|Y!^-e|Xee|Pv>nB9ZTK#G4npDhvp;$2HdfHD&9?1+jQ@^}bmAGWGSf!+Gsc)PE86`=Z{y0$aJ^gU`ZuS+tD(IQig7_%~A! zqqx=KjSYP5Ha0E;L|54yJc`anrbt3C0_E5pHg)hYR$*)bUWuP$i?9}V*)$x+CxUaH z4swaX=n92n1miH97>dkONKl6N6F;(fxaTh%yp=35@iin%0q)h7VYmMxdmZlfuO>Yr zKxG#Q#^8MFeBwK2q80=+7BI^_qEW*R=uWTK@%`GIo sYHyF8!*$5#i);%<^PgPEv9lfFNw~Rs>*^oG|9P6c2>;}B5FhyAKMkpJDF6Tf delta 631 zcmZXPPiPZC6vlUwbeqg(XR}Sx>_)V-NSf5zTEWE9QZh17h#K(hLA-?`f`uY@(7P7=1739ANcG?h`{V8ReQ(}OE5A7;9+@WB ztSm~2{JU?aH3G;N*XP1M0cd|zx$%InG`R7{M< zx=SLU)q?ZUUD*q5=zrRFm5SxH-_$BGFMLkRMYi?n+7++n`x)u`?F1T$71O~opJLr; zahKVsiZpB^!Scu$eRv|eaLo{6RYq_8-FT>{@zOZbw4$GxO#U(l*#QiSGArVi80RB& zI$=My#A7znT(PDZ8^^m?mwu8wjyiXduxDg*-9D}tRRf;e8DoOV0^uk=+I_r4C1C)m zj@PV&&7j--(UD{32`T16zUs~6VB!!vh6{;uObH=s>n9ZODv`C$Qb`z=U*kI7i(d33 zt8FCB-nEm>2gxdHn~{GxOz}>33OqF*RwIJ`B$iXd>kRQS>4YL?owuxvf_r*E9iU#O?xiw&DVQY0C4PoSFE4mY_1c&} dho*b4Kf=;qwEuCteCPZQ>0keuow%Cb`VIGMjE(>R diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index a04f671b56f9a17637b1a717c5331018fed82428..6f02ad61bcc467810e0188d2f0e841bdddac99fd 100644 GIT binary patch delta 241 zcmZ3?zePZOIWI340}!0OAdwlt%E0g##DM`3DC6_Ajq15f6A!Rzs-`k$iNT~()Yh;q zV`gAj4a5+TE)XT2q8`kksj+z;6FVcL`Q&@dvPSX6C5a`a#rihIB}Gi6yD=C8>ESsYOunD)q^J%#xe;Gix$3N=$yqV#z2zS(!DF4PjBtF7uRiQtGpq@^Tx+<^Qi*|+ zJYuO>Hg34hgJAe+k65mcrZ1ao-6Jh9zhce&RyxO5r`n*eYE54|aI4pFeZZ|*!|ecW z?HaBhxb8LFEx@fS<~mT}kxp^bFz5HsU-Eyg@bju`oWv6G0Ff>x!bB43TZY$#1{h~u z3>tkrznLzWw(>6erm2N*q`x!uzt{|�bQ1BK7d6+9##Sa}tryhvPEYf*eaI7FDE6 zifn=}FFa&3vP;CEeEwW4NJO#?zU}m!Fv2%3J`(zPejELpxs%^PTP$sUluC9Y!L?*u zD=)(=hsUFdIOzpu6}@O#0ImLE3G70Z{RU!%KZ2_g4-grhq%Urd#KvO@B`_71rpZTv z-$=h~ZH3v7gu2Cdtq1wk4q&Q6Y&t3tzmY9X+L2`iX?u7fc{%XM)m5Hgcrnj^KecR{{9A~_Z&oO`1?5bL4mWkH5MvX#JpmU`@UZBga8v0ggrQf{ZNSXuAoGM9f z!j*5E1EqyF%;+{OIOz7Wdb+Rl<3>x;f}-eOT(wM)6$Du~5M&J$gjx%Nb{hqyjef!F zcFbDl9b-J8i&gQ_OkK_1iK^VnE0N-ps~}0V*X0Isvn5G@q#5{DI#Eb!BOw>q6jH4e zIiWB%C!`=a%8T5r4fbb98k44^khCOiqVwC9S-bL){OVki6J1IBZE)m0bSB540Dy_o zBV-S>04K~^JdhpqS7irNp*;LSF9tEj_ft&D*Z6O6;~;Oe`V43|%QI@2H6{%UrU~fR zo(oL-A66#$R}I%|hB?v-{JXGF9f)RND8W6%5_X8(k*i!_SXLDTIqiAoYx#w0>rmg3e zbmx`y=(+S*Bs(TQ9+lIh^36dY+DkC-32IH?SQ0qDg$z-5`Sz4*j{?#KG`(kP89=O5 z1o6RNOHgHkkwL1F6!NnMKWmXN^kBe&v8xIP4jmpEE&>tUJHrjto`^?cK@sr!$K~ZI zwM2?0CM5!>q`HQW4<0`;9N2&G=%M{X!>TbNMKuU;Y5?;R3ORuU%|Tv4aterQ6=NZg z5>?1){4_;lAg(HuAOv{&1_NOLg-c`vCn0JucXY~_f($!!vR8AGae-i{z*(_J=1BEduix&`pjr%X!O0a zq4&gztav$db|!mv=JC)>dT8dg_H2bO-O>N%#Z1TEY{g)@VsM$`1`R`oxtEr$TxH!- zS=CZ?!%}(8QdK=RYMYl!tp`fY%bc~;Jomx}6`Z5uhA-{uelVV~4`l5FY107n_l)D9 z6coL&G4H;xD9@Aw^$v=Y;V5EaP>GR{k^a20#nX@c7t>9U$q_CEk@0C*DSzw~XL3g(F5(HR=aH6M>m@XTAvumD zpPuV0j0a&JnaTi@Qmddz2P78*q;egQ7%KTSEFv%CGRKe%Bf%8-ACyl497RzCRze6z z?wIyw8h75jm}%a5tLKg{JrYQVqK`+S>5=Hoav<8PZ5FFzmR#gTX!)&00P|Vw3LrU( zgcTs@e=-WhZ$=zQafAT@%j6KS)$$@7)6<+`trmegw1dd?x|VUz?v`2mn; zu%QpkoU8FWQ+NDnXLrWgoppB49auJVxvWni`hG~-!|=K|pSR4z@H^)0-V>SLliA*r zz|FX0S+H9CV%AO4rfL{%txMN$f3PD{-~HIqowjr@xm)JV*PTmN*H`V=>$aIf!yWEKKO&`iq)m< z^y}6B6oYGozSYGZmTy`WiU!xvmsy=-Qnbp831WzjKq_lyM*P%@Wv5>7>BlZc`XwCq z5Xc<&#MSsc+F$cPtNs?vwfp#vyxwq*TEkJunrKKWM95uT_aPd0`^PraJs&5F0q4yen?$%U%zdElSvnRn)08Y?1Hu(cg6=pKDmTDHIh@?oG z{jeB;1cZRqtuHC})<%n%e8# z%#5pB*QSR9j=?y%z4+S(2e0qzdE;TesbJtqR4KoioZqKuq^bSFhB~utG3yo-*Dnob zSQIY|(jrXMi{i=CfWi%iouTG~%DeG1DGT!N`m?D@|jm#xk) zYrW{R>7K{cKM%t*w5L_k&GYlt*ZEE~52Py}gS$Uv-X4 zN+3o8#aA|_aXD(V>Wa6X1udHE^Bc%59gM&wnO!5rB8%dAtU2B5)=Gd+6IBZfHa6mg?5c%^+7{_%eOuzhr5I))9Xf?cef~ zlnP#e0vIbJnP5pn@LI0r8Qu-py_;QKH4tFIfF}n*cw>;ka+XCn3nJ!AwsE#3JCL>G zZ!^sJyS0BZCMF`%0Qm}hn(GuSF6+tgy;;6@+2G=o9c_KQ-kNt%7im9x9i^T0FXZsrQ3}p0mQ}dd;iKXN5Tw+^p;p zMq5-b%34()(l*@Tui6nsZlH?Wj@;m-yO9$$RiX|Q)mCvkksGSwb|JT}irbCcbyeIr z6vc?tgR(GO6(h}&svR5Dh7mz&gplKJM^=Qp4Pi(+&q;0Qu4n#hXRj!>!aY|~48l68 zON_!((qMZB3Yim$;$kg$GyGH9)KQIa^+*-WIK$P-8mca~`xT{HD?G(1F39wO5- z(*C$-HRhpowlG!9LWu64&@CKPm(9dJI>3Rh~MmE^L# zq*h+0){APrYpC_6Z4=&=YPUMwdK~Kdn)b}dsKZ%ym8G(~EUUiD?irunTLB~pWkL0q zeRCM6n-nX4VPge`**07&qTx29%~J43-7c5nW<&5yeM&psm@P2vNh=kqre|`K$q9XG zQVBE47Yh@K5V7;!K#z??cwO?6q~I9R&fR-o99dSLL$s*_eyNY<%qsDl<+z9VT)3B3 zFB2IivJZ(NtA*_Jl$O`oEA;5f7qG5*3HkAvmyk(#d$L`0kjT^cFMSHBo~HS)!dbDs zzIh=OUWh~&>KYcpt+-eWdTQN^0+KT?{S|_Tonf=M=Z9N#v6~{r`)eoLNlU898zOrS zZ&>tvHpJmUt1`>IrYc$4jnrV~Bp9I;?+dr-F=;Uyf-B;_bC=vGSmuopv|7^2*Xt;L z6aS@KNX`iNYuZ1C3vHj{4A~w%ByL#3d_y$kT)w1d@>$J{$ZkAv!@@lT-?uBr*2L|? zt;~jOwlcGm#36IED|$FMT~u-Cq%E*xsHX_~V+fDWjFygt$Pz*xPj-+@4T0RL_&FRN zvJn{UY#QVv*fz=|wyIH8o78NhM^NJ#xY#*u<+8mi=7@8vF|tLIe|c7UwNPx>VNm0V zS-Y$|kZDNAat!AYj|j)cQBsetOg2xU9W;4GWg9;Gtz;+Oa300M$i1T1Ml9C0Joi2; z_q0bt@%i~gPe8Q%+TXiZ?69LbN|2VEsU%v3Z#VRuSQ9gMYXxv?aJ$hc4Q-Y5u9eYnCUkifU#s_pB+PbgPCcrd>2@-(&WTjDE$GhCGY%C7!SaEsf5|Xw(V8J zX!t7wloEP?fUX(NS}UBD6HLeLcc8ZaZBG+p#F%Ia{$|%ep1#!9XFc!~%jMucLcZ^UNbG?0nzA!7cFNP`*&5IY}#!0PXX)^CvJ9`cr6 zYS1%tCuSpo72}D_t(X%U$*>A6C%aNk2e5dGDC!#o^-W@ojDEvJw0TgIg3?4=tbsjkf8qeBq>X5BXY#`?3_IEWSpw|q!p!xdt_V#B1;b9ijLxB0mr;UuJfIHuv@bo1-V12&>PAilToaeLYa z@~q-C75oTC($}ngJ{)FOpl#jS3}Jf!yqNt85CmM6*&t@H72q54WXMEZHha3hw*bmg z7Z60Pr(7yucd(+1f%-(TP-Y)NwflfV1>MsH*>3G2-UE-9pMvljc`F=q7E+nF2p!@q zQgH9_;~=8RNR0t^%9|e~IHq~CluJWKBeLt+c{?cWL6AE^0Df6SVZ9s#`qpS)tr42) z<4;0HrO=NpZ+8j#O6L$wt=hdY7Nwp$Jf+o>`wN`{h|a(4jCo8u*&s_))}fDSm?hg@f#k zU!3Fi^^>bNPCi&aS>1ec^LTZ$K9p_ak&q`Uenl>0foM}-s ztW|6FjsC<&O=j0?<~BLN$%t=}LD`+MCv&>jwOi?{kMz8KXWrtw;s&S&{N)1>MZ+)* h*n;6bN4MVn71pw073f{@#5sQdlUEuz`yaHd`yZ`un`Qt2 literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 02632c4..34e63f5 100644 --- a/core/models.py +++ b/core/models.py @@ -23,10 +23,23 @@ class Membership(models.Model): return f"{self.user.username} - {self.company.name} ({self.role})" class Tender(models.Model): + STATUS_CHOICES = [ + ('opportunity-discovery', 'Opportunity Discovery'), + ('qualification', 'Qualification / Go–No Go'), + ('tender-registration', 'Tender Registration'), + ('bid-planning', 'Bid Planning'), + ('team-task-assignment', 'Team & Task Assignment'), + ('document-collection-drafting', 'Document Collection & Drafting'), + ('internal-review-compliance-check', 'Internal Review & Compliance Check'), + ('approvals-sign-off', 'Approvals & Sign‑off'), + ('submission-confirmation', 'Submission & Confirmation'), + ('post-bid-review-analytics', 'Post‑Bid Review & Analytics'), + ] company = models.ForeignKey(Company, on_delete=models.CASCADE) title = models.CharField(max_length=255) description = models.TextField() deadline = models.DateTimeField() + status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='opportunity-discovery') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/core/templates/core/tender_detail.html b/core/templates/core/tender_detail.html index 6bc6306..e59f04e 100644 --- a/core/templates/core/tender_detail.html +++ b/core/templates/core/tender_detail.html @@ -11,6 +11,7 @@

Description:

{{ tender.description }}

Deadline: {{ tender.deadline }}

+

Status: {{ tender.get_status_display }}

+
+
+

Workflow

+
+
+ {% if request.user.is_authenticated and request.user.groups.all|length > 0 %} + {% if 'Head of Bids' in request.user.groups.all.0.name or 'Bid Administrator' in request.user.groups.all.0.name %} + {% if next_status %} +

The current step is complete. Ready to move to the next step.

+ Approve and move to: {{ next_status.1 }} + {% else %} +

This tender is in the final workflow step.

+ {% endif %} + {% endif %} + {% else %} +

You do not have permission to advance the workflow.

+ {% endif %} +
+
+

Bids

diff --git a/core/urls.py b/core/urls.py index 2048395..f70b047 100644 --- a/core/urls.py +++ b/core/urls.py @@ -10,6 +10,7 @@ urlpatterns = [ path('create_company/', views.create_company, name='create_company'), path('company//tenders/', views.tender_list, name='tender_list'), path('tender//', views.tender_detail, name='tender_detail'), + path('tender//update_status//', views.update_tender_status, name='update_tender_status'), path('company//create_tender/', views.create_tender, name='create_tender'), path('tender//update/', views.update_tender, name='update_tender'), path('tender//delete/', views.delete_tender, name='delete_tender'), diff --git a/core/views.py b/core/views.py index 912dfa7..3f919e1 100644 --- a/core/views.py +++ b/core/views.py @@ -3,6 +3,7 @@ from django.contrib.auth import login, authenticate, logout from django.contrib.auth.decorators import login_required from .forms import SignUpForm, CompanyForm, TenderForm, BidForm, DocumentForm, NoteForm, ApprovalForm from .models import Company, Membership, Tender, Bid, Document, Note, Approval +from .decorators import group_required def home(request): return render(request, "core/index.html") @@ -81,16 +82,41 @@ def tender_detail(request, tender_id): doc_form = DocumentForm() note_form = NoteForm() + # Workflow logic + current_status_index = [i for i, (value, display) in enumerate(Tender.STATUS_CHOICES) if value == tender.status][0] + next_status = Tender.STATUS_CHOICES[current_status_index + 1] if current_status_index + 1 < len(Tender.STATUS_CHOICES) else None + + context = { 'tender': tender, 'bids': bids, 'documents': documents, 'notes': notes, 'doc_form': doc_form, - 'note_form': note_form + 'note_form': note_form, + 'next_status': next_status, } return render(request, 'core/tender_detail.html', context) +@login_required +@group_required(['Head of Bids', 'Bid Administrator']) +def update_tender_status(request, tender_id, next_status): + tender = get_object_or_404(Tender, pk=tender_id) + + # Find the index of the current status + current_status_index = [i for i, (value, display) in enumerate(Tender.STATUS_CHOICES) if value == tender.status][0] + + # Find the index of the next status + next_status_index = [i for i, (value, display) in enumerate(Tender.STATUS_CHOICES) if value == next_status][0] + + # Check if the next status is valid + if next_status_index == current_status_index + 1: + tender.status = next_status + tender.save() + + return redirect('tender_detail', tender_id=tender.id) + + @login_required def create_tender(request, company_id): company = get_object_or_404(Company, pk=company_id) diff --git a/requirements.txt b/requirements.txt index e22994c..cbc66a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 +requests==2.31.0