From a0ffebc489ce6595f8d95a2439aeb84953e76eb8 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Sat, 19 Sep 2026 12:07:53 +0200 Subject: [PATCH] feat: add monthly team planning scopes and operation types to company teams --- loko/contracts/admin.py | 51 ++++- .../contracts/locale/en/LC_MESSAGES/django.mo | Bin 32802 -> 34345 bytes .../contracts/locale/en/LC_MESSAGES/django.po | 84 ++++++++ .../contracts/locale/fr/LC_MESSAGES/django.mo | Bin 39771 -> 41387 bytes .../contracts/locale/fr/LC_MESSAGES/django.po | 84 ++++++++ .../contracts/locale/nl/LC_MESSAGES/django.mo | Bin 34070 -> 35684 bytes .../contracts/locale/nl/LC_MESSAGES/django.po | 84 ++++++++ ...nyteam_geom_companyteam_operation_types.py | 24 +++ loko/contracts/models.py | 46 ++++- loko/contracts/services_teams.py | 83 ++++++++ .../templates/contracts/teams_index.html | 98 +++++++-- loko/contracts/test_planning.py | 101 +++++++++- loko/contracts/views_teams.py | 91 +++++++-- loko/inspections/admin.py | 24 +-- .../locale/en/LC_MESSAGES/django.mo | Bin 21935 -> 23695 bytes .../locale/en/LC_MESSAGES/django.po | 13 ++ .../locale/fr/LC_MESSAGES/django.mo | Bin 27318 -> 29357 bytes .../locale/fr/LC_MESSAGES/django.po | 13 ++ .../locale/nl/LC_MESSAGES/django.mo | Bin 21456 -> 23097 bytes .../locale/nl/LC_MESSAGES/django.po | 13 ++ ...one_inspection_assigned_member_and_more.py | 32 +++ loko/inspections/models.py | 63 ++---- loko/inspections/permissions.py | 12 +- loko/inspections/services.py | 187 ++++++++++++++---- .../components/edit_inspection_modal.html | 4 +- .../inspections/inspections_list_mobile.html | 10 +- loko/inspections/tests.py | 113 +++++++++-- loko/inspections/views.py | 67 +++++-- .../0005_alter_signpole_support_type.py | 18 ++ 29 files changed, 1117 insertions(+), 198 deletions(-) create mode 100644 loko/contracts/migrations/0008_companyteam_geom_companyteam_operation_types.py create mode 100644 loko/inspections/migrations/0014_remove_inspection_zone_inspection_assigned_member_and_more.py create mode 100644 loko/sign/migrations/0005_alter_signpole_support_type.py diff --git a/loko/contracts/admin.py b/loko/contracts/admin.py index 9b04071..f91b7c2 100644 --- a/loko/contracts/admin.py +++ b/loko/contracts/admin.py @@ -264,18 +264,61 @@ class CompanyTeamMemberInline(admin.TabularInline): verbose_name_plural = "Membres" +from django.contrib.gis import admin as gis_admin +from django.utils.translation import gettext_lazy as _ + + +class CompanyTeamAdminForm(forms.ModelForm): + operation_types = forms.MultipleChoiceField( + choices=CompanyTeam.OPERATION_TYPES, + widget=forms.CheckboxSelectMultiple, + required=False, + label=_("Types d'opérations gérées"), + ) + + class Meta: + model = CompanyTeam + fields = '__all__' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance and self.instance.pk: + self.initial['operation_types'] = self.instance.operation_types or [] + + def clean_operation_types(self): + return list(self.cleaned_data.get('operation_types') or []) + + @admin.register(CompanyTeam) -class CompanyTeamAdmin(admin.ModelAdmin): - list_display = ('name', 'company', 'thematics_list', 'members_count', 'can_generate_daily_report', 'photos_required', 'restrict_today_interventions_visibility', 'visibility_start_hour', 'restrict_to_assigned_interventions') +class CompanyTeamAdmin(gis_admin.GISModelAdmin): + form = CompanyTeamAdminForm + list_display = ( + 'name', 'company', 'thematics_list', 'operation_types_list', 'has_geom', + 'members_count', 'can_generate_daily_report', 'photos_required', + 'restrict_today_interventions_visibility', 'visibility_start_hour', + 'restrict_to_assigned_interventions' + ) search_fields = ('name', 'company__name') - list_filter = ('company', 'can_generate_daily_report', 'photos_required', 'restrict_today_interventions_visibility', 'restrict_to_assigned_interventions') - filter_horizontal = ('thematics',) + list_filter = ( + 'company', 'can_generate_daily_report', 'photos_required', + 'restrict_today_interventions_visibility', 'restrict_to_assigned_interventions' + ) + filter_horizontal = ('thematics', 'asset_categories') inlines = [CompanyTeamMemberInline] def thematics_list(self, obj): return ", ".join([thematic.name for thematic in obj.thematics.all()]) thematics_list.short_description = 'Thématiques' + def operation_types_list(self, obj): + return obj.get_operation_types_display() or "—" + operation_types_list.short_description = _("Opérations gérées") + + def has_geom(self, obj): + return bool(obj.geom) + has_geom.boolean = True + has_geom.short_description = _("Zone définie") + def members_count(self, obj): return obj.members.count() members_count.short_description = 'Nb membres' diff --git a/loko/contracts/locale/en/LC_MESSAGES/django.mo b/loko/contracts/locale/en/LC_MESSAGES/django.mo index 015d60135e813abda4bd30d671d5cb927a2844d0..c8314f5d68833ea4f768d47ae5364093eb0b34c4 100644 GIT binary patch delta 11398 zcma*s2Xs``zQ^&C0!c_HA)!e-A&`V_kluT*QbJK7BtsHNX2N6w1cV_Vh%}W*5dkSG z1O(JU6cs^HilCq(O+_pymU}H+0hQ~0-`||gz4+F9>%FtqKcBt#IcJyu-e>ULw|Q=8 z>C#aD#meC;Ev^e8mQ@RXs9;&ep_cVpZM9m~!DP#N3*W%9xG=@Cc*I(UToxvJz-$i8hYHIyfE6;3`x{>reweg)ZEUHddjx z{@5DpVlnFdO{n({Vii1!)$k(5;x(+p_*P_N=SDJC=SCOQ;zwgSoQBa@jOuWiaV17k zUytf|D|W;Kn2I-%F|1}yoRyEk!PI@I2_3>1#<$K>NXD-*2`e>b(^hjXO{oeF-b!e)PAb@Sb@fjNY_|6|pTQqB3z0 z4#kf{0htCpBRoYq@z2=qB1%HHNo+zxYMonxq#-JDVyuStc*PgDXK`DRI zJa8N}v2&;ye~#LsAFwjU-{o|ijJltSnpj8Fiie}Vg3(w3$D;;zoBAxQNPV%NLL~}o zF&1}V9PT&u_fZ{ui5mD9jKebQTTiTkB*D55m5E1C8CYlBj@t8=QD4RDsOLUKoh|>j z6l@9=Ia&4bPE^W9VmyvVrOJmoOiNKSe;W1Ni&zs6pfdC^s^hTM&RJ=Q9209aDg%qK z8?Hn$0wSe5#AR7T!F{cxQ|P3&jX#6vj2`kJbu zGSvySRr9bqF2hKD|Ibj+i@Q;&Ie_ZmxM{zH)v5o4oiM7Mb7uOWo*RN%aVF~PSd8j# zA8KolAz!w2!L&!Tcg|KdEUWL|rl3QZjN04g#vZ6cn1(t76Hw22P%D{^VK@(~;X>4w zY(PzHn{hu@q5c7CqF0b#U#mg~@~@P)p`aP(pjKLhItwdMhvp@;@eFF9>!<-L@^hDf z^-(MAjbpF?HNd-A53gZ!tU>zpTtC#f(>jrVej}`HG_Y;fd9*RMGrxM+0<|TNp$6KB zI^A#LK|F=w_-Gd=Bdbu)Z$dq{6E)#iQ3HR7gHb=9oKtH^SHJU1w15W9a3j{mQ)uH) zsF}ugbAHQvpbnKAmC}_s5qDs1tkB&Vun8to?}<$@8(ZKi)Huhm7XId^paHA)uq?L4 zYK=<$RIGukP^bMl)Jl$^4%KIMM8}weo$KjHgg5xM7U!?X0vqDpSd*1#~m^M@?`za!jlVsEI#` zW$^$e>ia)TA&iFaP!sq&Dup*u6N&EQ%)BmYMXAUKV6{WtFT|1f5UQWgPy>I7+M1v6 z4*VUp=T-VT_gi3f#<#jq&`L+6Qtv@^^ZE|q@9M-2^1GP1sQT_EtzgCb&K`ESq4RHb1#NF5gkE2#_1EVmmzcXNM)ZQkdwx%U& zLOo4=0M4a84mI#6SPrkCGJUl_`H!FwGQdf36e{&KQ7<$?t)w+-;4Y}M(hqf*#$z~6 zMZGr-_1pr~01u-wx*nCe7myFd`Uth48Ux9{I%+x4Iqki%4)rmnJ_D7yCD;s~Lw$yy zp(cLa_%Bo@BL+FYd^NEp^`02~MK!KKEno-g>w3daK^>k(t>6dqz#pg=V+T9Ge2tLo zTLV#NV-;!wYf&9;MxCLTu>qdMA^0n5OL|e!a|2NsnTXmt|8xrKXp!*|R0`LiRik&?DfGqDsCw-*=bz&#*oFEQ?22C+>yLK+d)|eb$TqBudr=cQh86GvYUSTzC|*OI zjo(n23LnG%Yen&6oQB${!`Bpb%G+Qpc0grj5bEHK7(5kA3~7a4)JOFKVD7V=;2htcS1$eu`c3I#$QF_c<#Y zhRRqjYJ&Nw=jWg@u?F@0)297()EV)gqoBR`0hQ95#xmob2~|LCNfp%nx~M%)!B*G~ z^@HO<9kP|E{`O-JeAm=tCpdebfGM;$L0jMdND4aL(@-hhY9^GHT#Cs57wK_?T&5i%n_Yjyi_-iB0UO{yP>1Y}Y-eJ9QD#sF)P!BA3C>4#_=IsgCR5*sdj4~a#2--^xPj{L zPb{YoDQt?V@h!4VAG8s8e2m8h8%B7%FJRh6mdek1ii~7F8XSJ97YsN=tP|7cx2mWb{FK|}c7`4}}QD>nG#$rFLfa5S4 zC!@~HG^~iTQD4C_)4mdw*>$Kx_<8~P=M-9pXix|5q3RbCMRgo8&3Q2vb$F_y2Iz_XaTw|_ zt~2c$jXP0W^9okM6Q=zOQ~wc_=`z!U3-MdA6x30D)XY*)6KIS!wnCkeQK&uh8H-R` zGY7T8)u@g)pq}53+WVuZvvUfS(euXdF!=rdK|wRFHpBT|8=^AO8`bd$R0rcw17xC} z&qqyk9%|2*;uu_qO6@gcNRd;ILG@D&m1$e;`uUcD2;A~W;e5j6Q z7>iL`vlun8)u?|9N>Q0Oidy;C#_Omv6giXq*XK}^f;#AgHLyRXU?%FpWvEPSG;T$8 zxC?ct4q$Wq0=4IrXE`fOL1nTds{j6|1*M^W>a%9?{VNrDG*rc6tch!I2<}0xIQ)KR z?;|mudU<0#w5hkjjyM`M(UqtH*P${}iW>M;ROXJO&eG}o$-iDWPlHx^$vk)!HS@69 z&Wo|A6(^wDTcB3Z71d#H)Wn9M&P1kZcbocj)K79T>bM9J5~B>Zl*; zADvNX;~dlqocQ7c%6n%H{OO1EKff*3~qO>_S+>iPFknfV;G zh2LNl<6D1H2*t<+PKVJLPQ5DX#hO?K8>3Q~irT{=s0>X&4LljO1v5~4y9BkMEvSD( zE@3L@woHxD`V&j<`ZO8ncOhlk9`fgZ3@AJ^v-`KN8bv z%Oj>yP9R<&Hd3#N&k*|cewO-qrBByhDl>>~gf1P-al{JZv}wDG@_gdfl}175B8mGI zh{cqPiQ$AU7j19iOT+=n?-C~{^Y5MY8L^27{{NK+QP9MX_{2Og!nlq$UEdQ~ z!IJam5$M9b>9~VfO*};CdYrhMdkt_Op&!gcgswayiYOrbA>{u93e^c+GkNI+Vm;+8 z#1zWah}Vg0)IUTI>MBopA#sqnb-iHu{kxS?|MArp};BQqS<$n?noA>=O z6dDq*5%&^1iT*?;5B+IgtVF#JF_!ug{D@dYBvEgNx{8fXjZf6nSJJTDG&cTk&ve)K zuj?GKji|u`U!#7mb+xCie;f4cxPh2V-Gz_d)=?z&7@{#TkM{S8?YG^hIr!fwYtd`k zTc|O3G5$R?j=AlHk=*EFZi^?01H?k2H!+)M|Bm&E2+B7xpV0M+gH=j<2g2F2LDOzgbM#4vIC*Gs&Uw8qZ$Cdat9wv0vCaTjG zhaIsQF^H&0eGXAT`PMbfl;5J^B;{fF3>N7?K1Ss=qCAmL;{;+6WnDSM2I>olUnmd5 zG{Qryqdt|mb*-W;gNpvi)K!Q2b57BEnsO}B+0;{wlXW2aa-$caYk)BeKQQHe*qdlc z4CR?rjK)?tQ!kn8F6!rrs?>iXUZng0?lt#qEJHLU!nmg^!1$I;<#}Q~v4W^iJV)F| z+{68+iI0f3)Jurk#9?AKZKcHL#Dmo9p{}PW>pJQX{NG#mn6@6qIrui=FTFRH+`o0v+yJN6>pq&%E>gJ?os*Ly^tV9EK@h zPO7>hDZC^%DZZpm(u(jwo}7Zf&ZM+*5pJLN(4Jh^3#reBbuIKBDs|cE`T4myQwvqh z^>_>HOiWHMD9Fijr~7g|Zrf96XBOICm(QD?m3nhL%q5?xFt?J(4vkiHxDi#5@5<2o1tq(3s+Gia{4DTw z$GDKBjGT$NE?e)p3VnLVKD6EEb=jHe?gE=TPA`EUJN+5bFpp%||DP8EUw3H`S|g{x zQu>@XoMHjX$#89n>>d_9vB2e>7R*yYN!ea4;xuoks*EVX1%cMRR)j=&eadkAz}vkS zga^70_#w=n?D-!awnh}?yV!)k+ze*8z;?OqjO=u8mX_nSGh9BO%RW>(wJ;~&Wv94i zw72`Y|DB#DRD4X$R~N=7CMP=4wx&` zo}SGDoP(9)&aypT&QzXfn#$?H6Xo&-5;Ma?Iu5%1@Yq={bFMP%Ngj`nT)2Idy!NdV zlwmr}W38;kwIM7%c$A9hmVM_qxO9|C{MnsLe#?IOFGuKdSkLsLk`Sl?A&DRoB0(aFBqXsy5FtcF5Ti!zz4un5T=6q&T(u&qRT?pB$MrKy zY}%?-MO90=Ril^dDy?yi`+EP**Zu#`*5KQ{5fci+N^S94Fi? zD-_fHEUTxxWern-<8C?2I*#{{Q`WZfmgS52SO~wu;&>6g@Gg4eL*r8{P5BL0z|t|6 zRTLXxS<7-*?MWi2n1Erp91Gwc^u~NtgD0>*p2rAGjJ2$0*b&1q7j^$p)O|Ox1m447 ze1icP9A{q_hrzUOWssEQL@z9Z6HyI*gc@Nk>VeygyRjhUgQy38g>~@;R>Lw3lmDz% z{AlEpuq9@rI&=#Q;|q+TeapL|WidNeJo2B_fgfhunvUw&2CRukFcqI-JSJ7LXJQa) zY38CDT#mZWiG}bqYDTZ1KVHWg_z)esAiA=>hE*|%ayn`zGO;zTz$$nj6EK8=ro1IC z!x2~t3(%_~7>=G;8PhNUeQ_MBgHzE5SHv^_IiP}xR2`Db7Y@c?kuPtSON9G zdZ-RJL3QX|)JO-T7mh$b9EWOfrYSGRqLg#cAGc!w9&wQ9_4&?J+(A9yIjUiw1p9ml zrc;hbPFZ77Gq4^t3irM%}L2_Sil+5-g|*ao6DPZVnHDxhwxW6r0c zroIhovyDY9StjajS%O;Yb*O6PG18OOT zp?2{k)Cd<~46Z|6f5vzXHJ}Hm8F`87Xi!7jaHJy+E0%=Ww`!t#I2JYXr5J@P&;t*l zI&cj2;BQeKxq<5V6V!;j_)N0DEI-uwX4oEkpa-7>UhM9Up<}_yR0|*{J8NK#hDSs^j?>ji<2??OTsX^uSlB9{V=7 zrz{NPC?{bE_Q6Ux1=ZorSO8C=mhd!c39g|RJ}~7cIF+(z6Z@^2g?h^tqeD~rDTz03 zLQUlkR8NniUc>KDBlsD$Ywx1=#P6u*H^0JRj6&1{oV z18CQb`B#HOsL%s5Q6tDU7pz6y_yv~5?~rF%f1vh6{pR+A8lv8Y&R7mdVJlpQT8i7K z1|Ohi!lQ+~WF;LW8bO?~8WyBn8#R)Ks2e+)@^IAaG|jjIwP|;u_QD19#Ve@$enB66 zj)lbuP3SRFAH zbI}ujL2b^vsCJ&&^$yFAdD2LWqoyVX^~I`-dDsPupl@q?DN3RmtcHBVtPE2>57p5% zs0Oy9rv4;qFPuRQ;7`L_LRj7!>ir5AYqADG-Xy- z`(H8vs1HaJ)MgrvdSEu{lex~6@1fTGImV)AH+yC(qc(9%)C^5B&P6?MIo8I*SRNfO zNHmhN-R-HWgBnR1s^Jc(wI5&{X6naaW$LG+IcR9wDNMkjSRGek zDD7JpNc0vwL^V9Jr~SYgsI^~!$(V&&igTznzlQ4AW7O0>N9_Ttmt8Jl3`NyPp}v4g zs1B#2w_0vRQjCg@sI?h_TH~>(8>XSoe`v}%s0VIAt??1m+j0suV^2`e@$7ByB|p^j z;!q7I8XKTL?OQEKG}7*<5%odcFbLJaIMm3e80VuN^oc32Gi9eKA2gmeUPbM(yVwAq zqdJ`2hnd$1`;(}_(WncjpgOV;wd=FZ`8*7xd;lZxM-0c8s1b+uwU@9w>H|~9*a9_x z9;oYvp_XubU*?}pV9lpO4Q<47cnr0ol!H`57p2p ztcIDW>+(>~*@^1tQB=onpq}^4VG{2F_WK!xx*-L%_ARggwnsg%E2`mMs0R!+=O>{$ zFcrPs6kk!?j?cLLSH3+DVy)bpNVa||BjYR6&qwG(Ta@iSw-@kgx74bL$e69?Ph@UB>v zavrMV-=Y(LM$J&x5ZhI#0d7PM>>!rIpD;=9zxz=8lbMW~qJgMQG7+^Y7hybZ!Wg`Y zngRD=_JhNaZH&L9 z`53ArXHnnuUrqgMR09P^+70*{BaBs1uU)FK4Hl-{2etV|j%5CoOsAqeW}6ESpdaNk zs1E&vy6z5Y#4n7VqwELzVG8vjsHN$G>ezUU#7uO@9MtpHU=iFliuuA)qwRmymPWl!{ZaLUjpH$Z@(k45kZs(In#m)m`_7;`@-3>piw=?qlIy6o ze}h_zGGlBbQBxj+>R2l3flW}?w?(b#X!OB}=#SHlS*RIVivf5T^~Jq_ngPc{65aR; z^mLp^677RRHe{<5jRiR$QY#@~_WI;!=3qqh{(k>XTh?ynS6!)Y}tezkMUT}Ngt}lct2ckZuVW|7kQ8UvF=0cr+5 zGwy$%`PYf>sR+Wm*c{(rGB%%R|2dtFdVThx8qPa`TMnTbK83pe3WngX4w4ciZjNRab{#2D55#;mnQ=&KVj7TN4`#!UP?-E~--yyaT`uk&b zAx07>i5#Lh7nLFAlI!@4NOonG^=~{+L=o$_M#p$8jcrYN66%%hX3FJ^eKDK})=eD$ zB^(R*nNGwJ9f-dkN4fZ_sVGKX!Q`FD{fXzM%s0%cO!+Y$Kt2)HRN@?=V=HxsaGI-R ze>wFBiZ7wV_3u8_k09c?@Fl)W%p}*b0fUqi38v020p^m!5 zEut-vljRmx+|imdWr_EwnPsjbbsazO`^w~vu>sMZn9p@id_wdiUx~Hx2!bBtH^qRO+aYkL=X-JBD%*vfkJPw-WWpzj2*qcakqB zuW!n4DMWJnZ2`Y0Qn^+~ApVLMY5)I@B$I9CZT+o=gm*oPw>)`6`r;m}^q- zKK0A6DlSBKB9y!}(UH77QG_^5c>y}INp#e*`G3DrTu_S0ptcdQguFV@j+_nZ`h)&= zohNj}Vh@w6tbe0L64}&s#1q(%=tKBZ&UJHrXZZ9`kxoS$q9D1BvBXgFvKT>BBXq1b z=S!kHWzFD6We%bgkO2MbZbqCC(B$vUHLo zz{Xl(%6XKtOx>rZ?y&KmsTb3z+imJUpd3y#qdZP0%`u9yqY{Nh#8BcrD*6*25Pv;} zP);&+rEwRLOoW;`U3-vlP);P$$OoVg(TF&p3pnPI_i|($lF;{#{@fPj5_S*95`m5n=^H6u6x$Q@sZBx302*kUEkm8;Vd$_p-0v!$3^Gn Nsh2#QD`(Dk`yU#sqox1= diff --git a/loko/contracts/locale/en/LC_MESSAGES/django.po b/loko/contracts/locale/en/LC_MESSAGES/django.po index 0e183a1..2769536 100644 --- a/loko/contracts/locale/en/LC_MESSAGES/django.po +++ b/loko/contracts/locale/en/LC_MESSAGES/django.po @@ -2270,6 +2270,90 @@ msgstr "You do not have permission to generate documents for this order." msgid "Déclaration de créance créée et sauvegardée." msgstr "Debt declaration created and saved." +#: contracts/models.py:73 +msgid "Inspections" +msgstr "Inspections" + +#: contracts/models.py:75 +msgid "Contrôles" +msgstr "Controls" + +#: contracts/models.py:76 +msgid "Observations" +msgstr "Observations" + +#: contracts/models.py:93 +msgid "Zone géographique" +msgstr "Geographic zone" + +#: contracts/models.py:94 +msgid "Emprise géographique multi-polygone pour l'attribution automatique." +msgstr "Multi-polygon geographic footprint for automatic assignment." + +#: contracts/models.py:99 contracts/admin.py:274 +msgid "Types d'opérations gérées" +msgstr "Managed operation types" + +#: contracts/models.py:100 +msgid "Types d'opérations prises en charge par cette équipe (ex: Inspections, Interventions, Contrôles, Observations)." +msgstr "Operation types supported by this team (e.g. Inspections, Interventions, Controls, Observations)." + +#: contracts/admin.py:312 +msgid "Opérations gérées" +msgstr "Managed operations" + +#: contracts/admin.py:318 +msgid "Zone définie" +msgstr "Defined zone" + +#: contracts/templates/contracts/teams_index.html:679 +msgid "1 Mois" +msgstr "1 Month" + +#: contracts/templates/contracts/teams_index.html:684 +msgid "Portée :" +msgstr "Scope:" + +#: contracts/templates/contracts/teams_index.html:685 +msgid "Durée appliquée lors de l'assignation ou du retrait d'un membre" +msgstr "Duration applied when assigning or removing a member" + +#: contracts/templates/contracts/teams_index.html:686 +msgid "Jour" +msgstr "Day" + +#: contracts/templates/contracts/teams_index.html:687 +msgid "Semaine" +msgstr "Week" + +#: contracts/templates/contracts/teams_index.html:688 +msgid "Mois" +msgstr "Month" + +#: contracts/templates/contracts/teams_index.html:887 +msgid "Un mois complet" +msgstr "A full month" + +#: contracts/templates/contracts/teams_index.html:924 +msgid "Mois source" +msgstr "Source month" + +#: contracts/templates/contracts/teams_index.html:928 +msgid "Mois cible" +msgstr "Target month" + +#: contracts/views_teams.py:648 contracts/views_teams.py:664 +msgid "Impossible de modifier une période passée." +msgstr "Cannot modify a past period." + +#: contracts/views_teams.py:790 +msgid "Le mois cible ne peut pas être dans le passé." +msgstr "Target month cannot be in the past." + +#: contracts/views_teams.py:792 +msgid "La date cible ne peut pas être dans le passé." +msgstr "Target date cannot be in the past." + #~ msgid "Ce déplacement modifie l'équipe de" #~ msgstr "This move changes the team of" diff --git a/loko/contracts/locale/fr/LC_MESSAGES/django.mo b/loko/contracts/locale/fr/LC_MESSAGES/django.mo index f6449bcd33512e8a03abe6438aed3284a69d83d7..fba0a2258ffa323db86ab5588ba7e53e50402309 100644 GIT binary patch literal 41387 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 c4cc82L(t(>c< znFmy)FP1|MXaol0OpN3C&SDDXaX)g6a} NRk;?O{_+}H8#L$SP73}H2#Cyn#g4H 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 diff --git a/loko/contracts/locale/fr/LC_MESSAGES/django.po b/loko/contracts/locale/fr/LC_MESSAGES/django.po index d71651a..a8f0ad4 100644 --- a/loko/contracts/locale/fr/LC_MESSAGES/django.po +++ b/loko/contracts/locale/fr/LC_MESSAGES/django.po @@ -2320,6 +2320,90 @@ msgstr "" msgid "Déclaration de créance créée et sauvegardée." msgstr "Déclaration de créance créée et sauvegardée." +#: contracts/models.py:73 +msgid "Inspections" +msgstr "Inspections" + +#: contracts/models.py:75 +msgid "Contrôles" +msgstr "Contrôles" + +#: contracts/models.py:76 +msgid "Observations" +msgstr "Observations" + +#: contracts/models.py:93 +msgid "Zone géographique" +msgstr "Zone géographique" + +#: contracts/models.py:94 +msgid "Emprise géographique multi-polygone pour l'attribution automatique." +msgstr "Emprise géographique multi-polygone pour l'attribution automatique." + +#: contracts/models.py:99 contracts/admin.py:274 +msgid "Types d'opérations gérées" +msgstr "Types d'opérations gérées" + +#: contracts/models.py:100 +msgid "Types d'opérations prises en charge par cette équipe (ex: Inspections, Interventions, Contrôles, Observations)." +msgstr "Types d'opérations prises en charge par cette équipe (ex: Inspections, Interventions, Contrôles, Observations)." + +#: contracts/admin.py:312 +msgid "Opérations gérées" +msgstr "Opérations gérées" + +#: contracts/admin.py:318 +msgid "Zone définie" +msgstr "Zone définie" + +#: contracts/templates/contracts/teams_index.html:679 +msgid "1 Mois" +msgstr "1 Mois" + +#: contracts/templates/contracts/teams_index.html:684 +msgid "Portée :" +msgstr "Portée :" + +#: contracts/templates/contracts/teams_index.html:685 +msgid "Durée appliquée lors de l'assignation ou du retrait d'un membre" +msgstr "Durée appliquée lors de l'assignation ou du retrait d'un membre" + +#: contracts/templates/contracts/teams_index.html:686 +msgid "Jour" +msgstr "Jour" + +#: contracts/templates/contracts/teams_index.html:687 +msgid "Semaine" +msgstr "Semaine" + +#: contracts/templates/contracts/teams_index.html:688 +msgid "Mois" +msgstr "Mois" + +#: contracts/templates/contracts/teams_index.html:887 +msgid "Un mois complet" +msgstr "Un mois complet" + +#: contracts/templates/contracts/teams_index.html:924 +msgid "Mois source" +msgstr "Mois source" + +#: contracts/templates/contracts/teams_index.html:928 +msgid "Mois cible" +msgstr "Mois cible" + +#: contracts/views_teams.py:648 contracts/views_teams.py:664 +msgid "Impossible de modifier une période passée." +msgstr "Impossible de modifier une période passée." + +#: contracts/views_teams.py:790 +msgid "Le mois cible ne peut pas être dans le passé." +msgstr "Le mois cible ne peut pas être dans le passé." + +#: contracts/views_teams.py:792 +msgid "La date cible ne peut pas être dans le passé." +msgstr "La date cible ne peut pas être dans le passé." + #~ msgid "Ce déplacement modifie l'équipe de" #~ msgstr "Ce déplacement modifie l'équipe de" diff --git a/loko/contracts/locale/nl/LC_MESSAGES/django.mo b/loko/contracts/locale/nl/LC_MESSAGES/django.mo index 09b79aeec30e0e46b0015bf562cf6a5c79cfc980..d84dfcae56e565ebc8dda70dad51252d194a5f59 100644 GIT binary patch delta 11454 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 bool: + """Indique si l'équipe prend en charge le type d'opération donné.""" + if not self.operation_types: + return False + return op_type in self.operation_types + + def get_operation_types_display(self) -> str: + """Retourne la liste lisible des types d'opérations gérées.""" + mapping = dict(self.OPERATION_TYPES) + return ", ".join(str(mapping.get(op, op)) for op in (self.operation_types or [])) + def __str__(self): return f"{self.name} ({self.company.name})" diff --git a/loko/contracts/services_teams.py b/loko/contracts/services_teams.py index b443ba1..1159efe 100644 --- a/loko/contracts/services_teams.py +++ b/loko/contracts/services_teams.py @@ -2,6 +2,7 @@ Service de gestion temporelle et de planification des équipes. Gère les appartenances continues, l'historique d'équipe, les absences journalières et la duplication. """ +import calendar from collections import defaultdict from datetime import timedelta, date from typing import Optional, List, Dict, Set, Any @@ -182,6 +183,51 @@ def set_member_team_schedule( return new_history +@transaction.atomic +def set_member_team_period_schedule( + member: CompanyMember, + team_id: Optional[int], + start_date: date, + end_date: date, + action: str = 'move', + unassign_team_id: Optional[int] = None, + changed_by=None, + weekdays_only: bool = False, +) -> int: + """ + Affecte ou désaffecte un membre sur une plage de dates continue [start_date, end_date]. + Permet de définir des semaines complètes ou des mois entiers en une seule opération. + Ne modifie pas les jours passés (< aujourd'hui). + Retourne le nombre de jours mis à jour. + """ + today = timezone.now().date() + num_days = (end_date - start_date).days + 1 + updated_count = 0 + + for i in range(num_days): + d = start_date + timedelta(days=i) + if d < today: + continue + if weekdays_only and d.weekday() >= 5: + continue + + set_member_team_schedule( + member=member, + team_id=team_id, + target_date=d, + changed_by=changed_by, + sync_current=False, + action=action, + unassign_team_id=unassign_team_id, + ) + updated_count += 1 + + if start_date <= today <= end_date: + sync_member_current_teams(member, today) + + return updated_count + + def _sync_teams_if_needed(member: CompanyMember, target_date: date, today: date, sync_current: bool): """Synchronise member.teams si la date modifiée impacte la date d'aujourd'hui.""" if sync_current and target_date <= today: @@ -486,3 +532,40 @@ def duplicate_week_schedule( total_absences += res['absences_count'] return {'assigned_count': total_assigned, 'absences_count': total_absences} + + +@transaction.atomic +def duplicate_month_schedule( + source_year: int, + source_month: int, + target_year: int, + target_month: int, + allowed_team_ids: Optional[Set[int]] = None, + copy_absences: bool = False, + changed_by=None, +) -> Dict[str, int]: + """ + Duplique la configuration des équipes d'un mois source vers un mois cible (jour par jour). + Gère automatiquement les longueurs de mois différentes (28 à 31 jours). + """ + _, src_days = calendar.monthrange(source_year, source_month) + _, tgt_days = calendar.monthrange(target_year, target_month) + days_to_copy = min(src_days, tgt_days) + + total_assigned = 0 + total_absences = 0 + + for day_num in range(1, days_to_copy + 1): + s_day = date(source_year, source_month, day_num) + t_day = date(target_year, target_month, day_num) + res = duplicate_day_schedule( + source_date=s_day, + target_date=t_day, + allowed_team_ids=allowed_team_ids, + copy_absences=copy_absences, + changed_by=changed_by, + ) + total_assigned += res['assigned_count'] + total_absences += res['absences_count'] + + return {'assigned_count': total_assigned, 'absences_count': total_absences} diff --git a/loko/contracts/templates/contracts/teams_index.html b/loko/contracts/templates/contracts/teams_index.html index 8b36a02..245b6f5 100644 --- a/loko/contracts/templates/contracts/teams_index.html +++ b/loko/contracts/templates/contracts/teams_index.html @@ -676,6 +676,17 @@ + + + + +
+ {% trans "Portée :" %} +
+ + + +
@@ -862,7 +873,7 @@
-
+
@@ -871,6 +882,10 @@
+
+ + +
@@ -879,11 +894,11 @@
- +
- +
@@ -893,11 +908,25 @@
- +
- + +
+
+ + + +
+
+
+ + +
+
+ +
@@ -1572,6 +1601,7 @@ const initialViewMode = (function() { let _planningState = { anchorDate: initialDate, viewMode: initialViewMode, + assignScope: getStoredSetting('assign_scope', 'day'), companyId: getStoredSetting('company_id', ''), data: null, members: [], @@ -1584,6 +1614,18 @@ document.addEventListener('DOMContentLoaded', function() { $('#planning-viewmode-group button').removeClass('btn-primary').addClass('btn-outline-primary'); $(`#planning-viewmode-group button[data-mode="${_planningState.viewMode}"]`).removeClass('btn-outline-primary').addClass('btn-primary'); + // Restaurer le sélecteur global de portée d'assignation + $('#planning-assign-scope-group button').removeClass('btn-primary').addClass('btn-outline-primary'); + $(`#planning-assign-scope-group button[data-scope="${_planningState.assignScope}"]`).removeClass('btn-outline-primary').addClass('btn-primary'); + + // Gestion du sélecteur de portée d'assignation + $('#planning-assign-scope-group button').on('click', function() { + $('#planning-assign-scope-group button').removeClass('btn-primary').addClass('btn-outline-primary'); + $(this).removeClass('btn-outline-primary').addClass('btn-primary'); + _planningState.assignScope = $(this).data('scope'); + setStoredSetting('assign_scope', _planningState.assignScope); + }); + // Restaurer les filtres de la sidebar const savedSearch = getStoredSetting('search', ''); if (savedSearch) $('#planning-member-search').val(savedSearch); @@ -1621,7 +1663,7 @@ document.addEventListener('DOMContentLoaded', function() { loadPlanningData(); }); - // Changement de mode de vue (5j / 7j / 14j) + // Changement de mode de vue (5j / 7j / 14j / mois) $('#planning-viewmode-group button').on('click', function() { $('#planning-viewmode-group button').removeClass('btn-primary').addClass('btn-outline-primary'); $(this).removeClass('btn-outline-primary').addClass('btn-primary'); @@ -1677,13 +1719,10 @@ document.addEventListener('DOMContentLoaded', function() { // Mode duplication modal switch $('input[name="dupMode"]').on('change', function() { - if ($(this).val() === 'week') { - $('#dupSectionDay').addClass('d-none'); - $('#dupSectionWeek').removeClass('d-none'); - } else { - $('#dupSectionDay').removeClass('d-none'); - $('#dupSectionWeek').addClass('d-none'); - } + const val = $(this).val(); + $('#dupSectionDay').toggleClass('d-none', val !== 'day'); + $('#dupSectionWeek').toggleClass('d-none', val !== 'week'); + $('#dupSectionMonth').toggleClass('d-none', val !== 'month'); }); $('#dupSourceDate').on('change', function() { @@ -1721,8 +1760,13 @@ document.addEventListener('DOMContentLoaded', function() { function navigatePlanningDate(direction) { const d = new Date(_planningState.anchorDate); - const step = _planningState.viewMode === 'two_weeks' ? 14 : 7; - d.setDate(d.getDate() + (direction * step)); + if (_planningState.viewMode === 'month') { + d.setDate(1); + d.setMonth(d.getMonth() + direction); + } else { + const step = _planningState.viewMode === 'two_weeks' ? 14 : 7; + d.setDate(d.getDate() + (direction * step)); + } _planningState.anchorDate = d.toISOString().split('T')[0]; setStoredSetting('anchor_date', _planningState.anchorDate); _planningState.flatpickrInstance.setDate(_planningState.anchorDate); @@ -1982,6 +2026,7 @@ function executePlanningAssign(memberId, targetTeamId, targetDate, action) { team_id: parseInt(targetTeamId), date: targetDate, action: action || 'move', + scope: _planningState.assignScope || 'day', }, function(resp) { loadPlanningData(); @@ -2171,6 +2216,7 @@ function unassignMemberFromDate(memberId, dateStr, teamId) { team_id: null, date: dateStr, unassign_team_id: teamId ? parseInt(teamId) : null, + scope: _planningState.assignScope || 'day', }, function(resp) { loadPlanningData(); @@ -2273,6 +2319,15 @@ function openDuplicateModal() { const nextWeekDay = String(dWeek.getDate()).padStart(2, '0'); $('#dupTargetWeek').val(`${nextWeekYear}-${nextWeekMonth}-${nextWeekDay}`); + // Initialiser les mois source et cible + const srcMonthVal = `${parts[0]}-${parts[1]}`; + $('#dupSourceMonth').val(srcMonthVal); + const dMonth = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, 1); + dMonth.setMonth(dMonth.getMonth() + 1); + const nextMonthYear = dMonth.getFullYear(); + const nextMonthVal = `${nextMonthYear}-${String(dMonth.getMonth() + 1).padStart(2, '0')}`; + $('#dupTargetMonth').val(nextMonthVal); + $('#dupModalAlert').addClass('d-none'); new bootstrap.Modal(document.getElementById('duplicatePlanningModal')).show(); } @@ -2307,8 +2362,17 @@ function quickDuplicateToNextDay(sourceDateStr) { function submitDuplicatePlanning() { const mode = $('input[name="dupMode"]:checked').val(); - const sourceDate = mode === 'week' ? $('#dupSourceWeek').val() : $('#dupSourceDate').val(); - const targetDate = mode === 'week' ? $('#dupTargetWeek').val() : $('#dupTargetDate').val(); + let sourceDate, targetDate; + if (mode === 'month') { + sourceDate = $('#dupSourceMonth').val(); + targetDate = $('#dupTargetMonth').val(); + } else if (mode === 'week') { + sourceDate = $('#dupSourceWeek').val(); + targetDate = $('#dupTargetWeek').val(); + } else { + sourceDate = $('#dupSourceDate').val(); + targetDate = $('#dupTargetDate').val(); + } const copyAbsences = $('#dupCopyAbsences').is(':checked'); if (!sourceDate || !targetDate) { diff --git a/loko/contracts/test_planning.py b/loko/contracts/test_planning.py index 80bc724..958526c 100644 --- a/loko/contracts/test_planning.py +++ b/loko/contracts/test_planning.py @@ -15,6 +15,8 @@ from contracts.services_teams import ( toggle_member_absence, duplicate_day_schedule, duplicate_week_schedule, + duplicate_month_schedule, + set_member_team_period_schedule, sync_member_current_teams, ) @@ -176,6 +178,41 @@ class TeamPlanningServicesTests(TestCase): self.assertIn(self.team_b, teams_day2) + def test_set_member_team_period_schedule(self): + """Tester l'affectation sur une période continue.""" + start_d = date(2026, 10, 5) # Lundi + end_d = date(2026, 10, 11) # Dimanche + count = set_member_team_period_schedule( + member=self.member_alice, + team_id=self.team_a.id, + start_date=start_d, + end_date=end_d, + ) + self.assertEqual(count, 7) + for day_offset in range(7): + d = start_d + timedelta(days=day_offset) + self.assertEqual(get_member_team_for_date(self.member_alice, d), self.team_a) + + def test_duplicate_month_schedule(self): + """Tester la duplication d'un mois entier vers un autre mois.""" + # Configurer Alice dans Team A tous les jours d'octobre 2026 (31 jours) + for day in range(1, 32): + d = date(2026, 10, day) + set_member_team_schedule(self.member_alice, self.team_a.id, d) + + # Dupliquer vers novembre 2026 (30 jours) + res = duplicate_month_schedule( + source_year=2026, + source_month=10, + target_year=2026, + target_month=11, + ) + self.assertEqual(res['assigned_count'], 30) + + # Vérifier que le 15 novembre Alice est bien dans Team A + self.assertEqual(get_member_team_for_date(self.member_alice, date(2026, 11, 15)), self.team_a) + + class TeamPlanningAjaxViewsTests(TestCase): def setUp(self): from common.models import UserConfig, Role @@ -255,8 +292,12 @@ class TeamPlanningAjaxViewsTests(TestCase): def test_planning_duplicate_friday_to_monday_ajax(self): """Vérifie la duplication d'un vendredi vers le lundi suivant via l'endpoint AJAX.""" - friday = date(2026, 9, 11) - monday = date(2026, 9, 14) + today = timezone.now().date() + days_ahead_to_friday = (4 - today.weekday()) % 7 + if days_ahead_to_friday == 0: + days_ahead_to_friday = 7 + friday = today + timedelta(days=days_ahead_to_friday) + monday = friday + timedelta(days=3) set_member_team_schedule(self.member_alice, self.team_b.id, friday) toggle_member_absence(self.member_alice, friday, is_absent=True, reason='conge', comment='Weekend prolonge') @@ -346,7 +387,8 @@ class TeamPlanningAjaxViewsTests(TestCase): 'target_date': past_date, }), content_type='application/json') self.assertEqual(resp_dup.status_code, 400) - self.assertIn('passé', resp_dup.json().get('error', '').lower()) + error_msg = resp_dup.json().get('error', '').lower() + self.assertTrue('passé' in error_msg or 'past' in error_msg) def test_reassign_future_member_to_same_team_as_previous_day(self): """ @@ -559,5 +601,58 @@ class TeamPlanningAjaxViewsTests(TestCase): modal_member_ids = [m['id'] for m in resp_modal.json()['current_members']] self.assertIn(member_olivier.id, modal_member_ids) + def test_planning_data_ajax_month_view(self): + """Tester le retour du mode 'month' dans team_planning_data_ajax.""" + url = reverse('contracts:team_planning_data_ajax') + response = self.client.get(url, {'date': '2026-09-15', 'view_mode': 'month'}) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(len(data['days']), 30) # Septembre compte 30 jours + self.assertEqual(data['days'][0]['date'], '2026-09-01') + self.assertEqual(data['days'][-1]['date'], '2026-09-30') + + def test_planning_assign_member_ajax_week_scope(self): + """Tester l'assignation avec portée 'week' via l'API AJAX.""" + # Prendre un lundi dans le futur + today = timezone.now().date() + next_monday = today + timedelta(days=(7 - today.weekday())) + url = reverse('contracts:team_planning_assign_member_ajax') + payload = { + 'member_id': self.member_alice.id, + 'team_id': self.team_a.id, + 'date': next_monday.isoformat(), + 'scope': 'week', + } + response = self.client.post(url, data=json.dumps(payload), content_type='application/json') + self.assertEqual(response.status_code, 200) + self.assertTrue(response.json()['success']) + self.assertEqual(response.json()['scope'], 'week') + self.assertEqual(response.json()['updated_count'], 7) + + # Vérifier que le mercredi et le dimanche de cette semaine ont bien Alice + self.assertEqual(get_member_team_for_date(self.member_alice, next_monday + timedelta(days=2)), self.team_a) + self.assertEqual(get_member_team_for_date(self.member_alice, next_monday + timedelta(days=6)), self.team_a) + + def test_planning_duplicate_ajax_month_mode(self): + """Tester la duplication de mois via l'API AJAX.""" + # Créer des affectations pour octobre 2026 + oct_1 = date(2026, 10, 1) + set_member_team_schedule(self.member_alice, self.team_b.id, oct_1) + + url = reverse('contracts:team_planning_duplicate_ajax') + payload = { + 'mode': 'month', + 'source_date': '2026-10', + 'target_date': '2026-11', + 'copy_absences': False, + } + response = self.client.post(url, data=json.dumps(payload), content_type='application/json') + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertTrue(data['success']) + self.assertEqual(data['mode'], 'month') + self.assertGreaterEqual(data['assigned_count'], 1) + self.assertEqual(get_member_team_for_date(self.member_alice, date(2026, 11, 1)), self.team_b) + diff --git a/loko/contracts/views_teams.py b/loko/contracts/views_teams.py index 14349cb..78866f5 100644 --- a/loko/contracts/views_teams.py +++ b/loko/contracts/views_teams.py @@ -1,3 +1,4 @@ +import calendar import json from datetime import date, timedelta, datetime @@ -21,12 +22,14 @@ from contracts.permissions import ( ) from contracts.services_teams import ( set_member_team_schedule, + set_member_team_period_schedule, get_member_team_for_date, get_teams_composition_for_period, get_member_absences_for_period, toggle_member_absence, duplicate_day_schedule, duplicate_week_schedule, + duplicate_month_schedule, sync_member_current_teams, sync_all_members_current_teams, ) @@ -456,6 +459,11 @@ def team_planning_data_ajax(request): num_days = 14 start_date = monday end_date = monday + timedelta(days=13) + elif view_mode == 'month': + _, days_in_month = calendar.monthrange(anchor_date.year, anchor_date.month) + num_days = days_in_month + start_date = date(anchor_date.year, anchor_date.month, 1) + end_date = date(anchor_date.year, anchor_date.month, days_in_month) else: # 'week' (default) num_days = 7 start_date = monday @@ -602,6 +610,7 @@ def team_planning_assign_member_ajax(request): member_id = data.get("member_id") raw_team_id = data.get("team_id") date_str = data.get("date") + scope = data.get("scope", "day") # 'day', 'week', 'work_week', 'month' if not member_id or not date_str: return JsonResponse({"error": "Paramètres member_id et date requis."}, status=400) @@ -611,7 +620,7 @@ def team_planning_assign_member_ajax(request): return JsonResponse({"error": "Date invalide."}, status=400) today = timezone.now().date() - if target_date < today: + if scope == "day" and target_date < today: return JsonResponse({"error": "Impossible de modifier la planification d'un jour passé."}, status=400) member = get_object_or_404(CompanyMember, id=member_id) @@ -631,16 +640,48 @@ def team_planning_assign_member_ajax(request): raw_unassign_team_id = data.get("unassign_team_id") unassign_team_id = int(raw_unassign_team_id) if raw_unassign_team_id is not None else None - # Effectuer la planification - set_member_team_schedule( - member=member, - team_id=team_id, - target_date=target_date, - changed_by=request.user, - sync_current=True, - action=action, - unassign_team_id=unassign_team_id, - ) + # Effectuer la planification selon la portée choisie + if scope in ("week", "work_week"): + monday = target_date - timedelta(days=target_date.weekday()) + week_end = monday + timedelta(days=4 if scope == "work_week" else 6) + if week_end < today: + return JsonResponse({"error": _("Impossible de modifier une période passée.")}, status=400) + updated_count = set_member_team_period_schedule( + member=member, + team_id=team_id, + start_date=monday, + end_date=week_end, + action=action, + unassign_team_id=unassign_team_id, + changed_by=request.user, + weekdays_only=(scope == "work_week"), + ) + elif scope == "month": + _, days_in_month = calendar.monthrange(target_date.year, target_date.month) + month_start = date(target_date.year, target_date.month, 1) + month_end = date(target_date.year, target_date.month, days_in_month) + if month_end < today: + return JsonResponse({"error": _("Impossible de modifier une période passée.")}, status=400) + updated_count = set_member_team_period_schedule( + member=member, + team_id=team_id, + start_date=month_start, + end_date=month_end, + action=action, + unassign_team_id=unassign_team_id, + changed_by=request.user, + ) + else: + set_member_team_schedule( + member=member, + team_id=team_id, + target_date=target_date, + changed_by=request.user, + sync_current=True, + action=action, + unassign_team_id=unassign_team_id, + ) + updated_count = 1 return JsonResponse({ "success": True, @@ -648,6 +689,8 @@ def team_planning_assign_member_ajax(request): "member_name": member.name, "team_id": team_id, "date": target_date.isoformat(), + "scope": scope, + "updated_count": updated_count, }) @@ -728,6 +771,13 @@ def team_planning_duplicate_ajax(request): if not source_date_str or not target_date_str: return JsonResponse({"error": "Dates source et cible requises."}, status=400) + # Si mode == 'month' et dates au format 'YYYY-MM', normaliser avec le 1er du mois + if mode == "month": + if len(source_date_str) == 7: + source_date_str = f"{source_date_str}-01" + if len(target_date_str) == 7: + target_date_str = f"{target_date_str}-01" + source_date = parse_date(source_date_str) target_date = parse_date(target_date_str) @@ -735,8 +785,11 @@ def team_planning_duplicate_ajax(request): return JsonResponse({"error": "Format de date invalide."}, status=400) today = timezone.now().date() - if target_date < today: - return JsonResponse({"error": "La date cible ne peut pas être dans le passé."}, status=400) + if mode == "month": + if (target_date.year, target_date.month) < (today.year, today.month): + return JsonResponse({"error": _("Le mois cible ne peut pas être dans le passé.")}, status=400) + elif target_date < today: + return JsonResponse({"error": _("La date cible ne peut pas être dans le passé.")}, status=400) # Récupérer les équipes que l'utilisateur a le droit d'éditer viewable = get_viewable_teams_for_user(request.user) @@ -752,7 +805,17 @@ def team_planning_duplicate_ajax(request): else: allowed_team_ids = set(viewable.values_list('id', flat=True)) - if mode == "week": + if mode == "month": + res = duplicate_month_schedule( + source_year=source_date.year, + source_month=source_date.month, + target_year=target_date.year, + target_month=target_date.month, + allowed_team_ids=allowed_team_ids, + copy_absences=copy_absences, + changed_by=request.user, + ) + elif mode == "week": # S'assurer de pointer sur les lundis source_monday = source_date - timedelta(days=source_date.weekday()) target_monday = target_date - timedelta(days=target_date.weekday()) diff --git a/loko/inspections/admin.py b/loko/inspections/admin.py index ecad04a..c26aad5 100644 --- a/loko/inspections/admin.py +++ b/loko/inspections/admin.py @@ -1,14 +1,5 @@ from django.contrib import admin -from django.contrib.gis import admin as gis_admin -from .models import Inspection, InspectionZone, ParkingSpotInspection, GiepInspection, HotBoxInspection, InspectionDocument - - -@admin.register(InspectionZone) -class InspectionZoneAdmin(gis_admin.GISModelAdmin): - list_display = ("code", "name_fr", "thematic", "is_active", "created_at") - list_filter = ("thematic", "is_active") - search_fields = ("code", "name_fr", "name_nl") - filter_horizontal = ("municipalities", "inspectors") +from .models import Inspection, ParkingSpotInspection, GiepInspection, HotBoxInspection, InspectionDocument class InspectionDocumentInline(admin.TabularInline): @@ -35,21 +26,22 @@ def make_compliant(modeladmin, request, queryset): @admin.register(Inspection) class InspectionAdmin(admin.ModelAdmin): list_display = ( - "id", "asset", "status", "zone", "scheduled_start_date", "scheduled_due_date", + "id", "asset", "status", "assigned_team", "assigned_member", + "scheduled_start_date", "scheduled_due_date", "inspector", "inspection_date", "result_status", "mission_intervention", "checked_intervention", "apply_changes_to_asset" ) list_filter = ( - "status", "result_status", "nature", "zone", "inspector_role", + "status", "result_status", "nature", "assigned_team", "inspector_role", "apply_changes_to_asset", "inspection_date" ) search_fields = ( "notes", "inspector__username", "inspector__first_name", - "inspector__last_name", "mission_intervention__code", - "checked_intervention__code" + "inspector__last_name", "assigned_team__name", "assigned_member__name", + "mission_intervention__code", "checked_intervention__code" ) - raw_id_fields = ("inspector", "mission_intervention", "checked_intervention", "zone") + raw_id_fields = ("inspector", "mission_intervention", "checked_intervention", "assigned_team", "assigned_member") inlines = [InspectionDocumentInline] actions = [make_cancelled, make_compliant] @@ -95,7 +87,7 @@ class HotBoxInspectionAdmin(admin.ModelAdmin): "cracking", "water_drainage", "inspection_date" ) search_fields = ("notes", "inspector__username", "inspector__first_name", "inspector__last_name", "checked_intervention__code") - raw_id_fields = ("inspector", "mission_intervention", "checked_intervention", "zone") + raw_id_fields = ("inspector", "mission_intervention", "checked_intervention", "assigned_team", "assigned_member") inlines = [InspectionDocumentInline] actions = [make_cancelled, make_compliant] diff --git a/loko/inspections/locale/en/LC_MESSAGES/django.mo b/loko/inspections/locale/en/LC_MESSAGES/django.mo index 02ac4aa1eec963d178911a0163d1664c46707ade..8c770f6d0f1d0c66ef2f0969d410118a8e457db5 100644 GIT binary patch delta 6890 zcmZ|U30PKD9>?*+BBIESh@$ZVg191rf*J{_D2AH3r{zkf3~oqTY1hiirL+v0l+?m3 zwHi%EvkWY0hSAbe%k~V%)Jj`5(J=a7 z^9cVnhSbx?nAUizi7}hVzYwWfW3pNqlZ!nu2Jc30F=f^b7)E&u2IDIjj&&G;A7M*8 zhyEB4OJER28RM7;GA*cRXD{@z=AjxMY`xVw3)84yjQwyYUWb=Z9rj=rG9Rfj<52@z zfQ@lE(y>|XEjz|MM<$Sp?U;^xP%}A)n(-A>gOP2F;h#z7M+57J>R==`!a10T^HDRb zz!0oNCU0Is4fJhW4!VX_pnsE0MmKgttwgSMEc#KNgK=1jTKY9e8)hR8$7+nmaBk8> zQcyEY!vxI3rZ^q774z)%mFQ^1o5-l+IvkBBP&3N(xHHN|bubAv;|10-WO2+6RL6%< z9UsT$_zfnbPrTbs3bvzMfLf7x@$7$RGOMV#6{~R!wr1GPuoSg-_n{srLv^qnHSjkv z70)7LHZfFcYqC%s<)a7ZpeC>yHL$IymE4!;xHCUMg;t;*HKWU@Es1XH?tKz!39~Q@ z`=VZug_wkoVi4{|wX+{}20lg&yxx|7MIAyv9@2e54jGLs5!FyHTb_t&a6W3r_oD{( z1Zw2FPz~)v4YUqf1alm<0+*4#OjFihd!CBk6+zuU2Q?vQvCXVNo%V9{;C9r~zl+-Q zlc*VAMZFCXv_{q>A-iS@kUq^!)PT!S_isf_q#D)!0ek%40eBHP_~uvC3dXi~2igs_QbTOH$d(_%F4RARYUdcn(Z31k;4W1% zs^c556V6BenP>RXA$tdP{deq*9VyU;nTERWG1Pspp$2>odtn%j$s15-;X%{@pT=nV zH#KB*df!L>nT!17Vgj#*Mm!a@w~wRhccYg07u0DE<|eIRB8Fmr)E13IorT*m1n)%M zzZ4mp*?>-CG6%`%!K2ob*q8E;NFSyL(+b3?s0NBrhwFZ9hFehW?7~J^js39}wN*{J zx?2>68c;jb7G!m0{XJxgsLK_JgDJCzKYE$A3;5E23Z{Q zGit9p@Ez9S%tCd18|wbMF$Nz)y*1mgA09#tG@_?_Uu&=Ie>XB(fq|&^a~$fl-+>yC zgF5ZysJ+{ZEwK)Dz25o@wxAr!%dW%L4s~A!>QE0st;k&Lh!q%5|E7+NX7Cj*#;dp# z9lkQxVjw>^Vt>?GDMxj@3w3DU#&CSkUO$ccAT{D3Xuw@j&s~d?aU8bu;k$zleVNnO zxedhkahE6^n^K>RTKa*gB_D0eg{Zxshw69<>OC(<4QM@TMK_~XWDjaZ-?i62LbY4p zhy7Or-%}xdbKE~FQP{=snMIxA8K@3dqXt%qk@zBNU~ixX@D8e-w()#svXBdP!Ty0P@%|xC498^c6 zP+z_x)QUWWgK;ftrOu!Soo~tLfzW>L18q?=$U*Jl2-Hlc+43AzgGz}QVY9Dp-WhjJ&frH-i~qXrLKzd$wgBPL@=p1b$mQ3L9a8t@p@$`zyD zhNY;EH=dU~ zY5HLj#|o~8{9JxfqJkD zYUy)O0~(9k^AgmaF2^{m#3bC0TCua3f&m5YKvJ#4upRX!1+2d^6;x=CYSDvdPz?t1 z&ZvPDOvYiTh8LjDN;!7IgQyj{gdU6@;;uj@YM=$Em6(F5_yFpV?{dhf!6TT4-=UT+ zeyF=dJurcC0fyo%)O82_u?$0SEow=(pgO9t*T29}%3q@HyNH^Q|Bdb$aypYyLjzGa z4#u{43u@`_Lv2Z!wF31%SK9KIsQb=YucB5mY?#|Y25N#st;Og|c?AaR{a;BYh>9oO z3bVz!+xjLpLtvGh6l<<<2w(^;Tq~R%8n5 z43waw89hlxui+lljmJrqM|+ej&b+66KaONF$yQ5mbesSa2e|LtVGRtKdQsi*7Mk$@&(iY zn~rrS7>l~E2Wnv1V_AQ#z)&i*mqi$j^H2j@fm(@mr~z!lrdWfT$pO^q{uH$ZS5SK% zf3y2x%0YEB1}EYSoQ7|qz8mSzIQQ@TL~KRHQ>cdbqF&3x$ZKMbVJz08mi{-?fRo0% z_jf_QJth}@P;Hzg-XNwBe@=^R;W~7d*}_%KB>LF0_A{MGptZGFN%SS&BmQ5S&xHbF zqP;<9XBV-Oc$UZ`lsKQ>)X|6ipF#FkB8rZR@OMIIVLj21*d^n`_1AC>QBJS`-f#ar zY)E(7%u#%kc!$vTC@pa@?_)G=eT*;b{r?x4bSmQP4LPVi+(+mvoFo2D{DU}7CL7XAFYX76iBoI#$wM0Yukj!6* zg@nKD{7ZW=j&dydJ+}P4wH}`)wi9}7?G3l6*r7vH2v; ze>S!MAzrap@5Y~r5yW>y7NOLV@Z-To-V%~;Pjn%MPzo`Rt|E98;y%vDE zl)usXKR{+bv4v0y(GNFlgV%{yi8RV##4++ii6@AMi9$jt&Bgosl+8bcO?XDB2Y!Sd ziGLD)M2xO!|C7jM*c;!X+=bXdMA*7RQ0e&8q+{$+gLsoj}bkIVZ=*B6C#-KCzP7od#|`fvlSmC&J&%urvD#l6#1_R zr5}kOh&mOLQl`u*ojI>?Zu=6?^qD2I3#U}Aid+^ITs(Php=VM_$@FP6YqwT4PdOjl zVovGw*@dQdnb$wA6;}P;J I>XF<33lR$8XaE2J delta 5100 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 YnTkxNE;aO{qPh#?j^(fY>$ENZ1LC_P5&!@I diff --git a/loko/inspections/locale/en/LC_MESSAGES/django.po b/loko/inspections/locale/en/LC_MESSAGES/django.po index 2d0ebb8..d847cac 100644 --- a/loko/inspections/locale/en/LC_MESSAGES/django.po +++ b/loko/inspections/locale/en/LC_MESSAGES/django.po @@ -1970,6 +1970,19 @@ msgid "" "d'évaluation)." msgstr "Visual checks not yet filled in (scheduled or pending evaluation)." +#: inspections/models.py:163 +#: inspections/templates/inspections/inspections_list_mobile.html:309 +msgid "Équipe assignée" +msgstr "Assigned team" + +#: inspections/models.py:171 +msgid "Membre assigné" +msgstr "Assigned member" + +#: inspections/templates/inspections/components/edit_inspection_modal.html:675 +msgid "Équipe" +msgstr "Team" + #~ msgid "Renforcement de l'entretien préventif" #~ msgstr "Reinforcement of preventive maintenance" diff --git a/loko/inspections/locale/fr/LC_MESSAGES/django.mo b/loko/inspections/locale/fr/LC_MESSAGES/django.mo index 9c33eb74e9333d6f3513d7af02af5165f3479219..9cf3ed3a5496aff180614cca38972a6b6d931d25 100644 GIT binary patch delta 7920 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 diff --git a/loko/inspections/locale/fr/LC_MESSAGES/django.po b/loko/inspections/locale/fr/LC_MESSAGES/django.po index 3b1c422..f6df44f 100644 --- a/loko/inspections/locale/fr/LC_MESSAGES/django.po +++ b/loko/inspections/locale/fr/LC_MESSAGES/django.po @@ -1980,6 +1980,19 @@ msgstr "" "Contrôles visuels non encore renseignés (inspection planifiée ou en attente " "d'évaluation)." +#: inspections/models.py:163 +#: inspections/templates/inspections/inspections_list_mobile.html:309 +msgid "Équipe assignée" +msgstr "Équipe assignée" + +#: inspections/models.py:171 +msgid "Membre assigné" +msgstr "Membre assigné" + +#: inspections/templates/inspections/components/edit_inspection_modal.html:675 +msgid "Équipe" +msgstr "Équipe" + #~ msgid "Renforcement de l'entretien préventif" #~ msgstr "Renforcement de l’entretien préventif" diff --git a/loko/inspections/locale/nl/LC_MESSAGES/django.mo b/loko/inspections/locale/nl/LC_MESSAGES/django.mo index febaab22de16cd914996c5f9687a4617e6da73c2..09e44b39ac360d13974d59e11fcd8ac701f2fef7 100644 GIT binary patch delta 6397 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 delta 4745 zcmYM$3s6?)@2#|*e5+HJT91t|*m7{*Jzz7;Kw-Y&{bYcx|+6LqwWI8kQQ zbTwa%VvW@}HD$q6Ub}kLuGJV_GK=oj%;?3`Oo}y4YH8nJd(GrHeAe1~pSAw$|Nrj; z2Rl4nFL<1D@u5!{{`ow{q+)8MW*Gmcl?>CEC*xeyBde{=7{~lEjKSSF5MM<8n}6|R zFm_=meuv?B0TYaI%+Cx4aUuFf_r?%wE^6TdYn62-7I1w&*5EcAgJ+PXObV+?KPr$4 z?1#0;e>2CGMde=1tVePNPoViz?Yw%*3p8H{cpnfQwMiI}QUyz6G^#n|8BK`Koo66($Bbq$k{dzPLJuR)nt!4) z@_*FE*Kq{qahzNXqIzZ{>V7+p#?Ns$ddaVz8;g2w87kntScY%^p~J!qbkZ{JD#F`Q zkuOIDvI({Eeyqe#Pyr_s9UnF0QP&rtQu`9>tvQOsV!AOFFQZBmN#m&GshG(A#?L?} zs75u(Qq%_;u^JynyZ_dkbfs&=IB%sZ&se~ntlL*wZF z0MzVoVgb{oSBV!ZUeHm*={|G z#B9Dtt(!n2sTqf%4qoRlQ0f<>0{90ipaZCdx=^XUf{_?SBWS?{)Pm)xrk#!|&3&ln z)}b=eg37>N>l>&*Kfoe%zG4u=AerMbG&!iqW}zNjh$@ML>h>L2gNINX={R~W3KftK z^)?k@1WrW-P={*PB^ZrckN_RiW*<0c?LvLkevAF_I_f0-^WEmmL1kbfl2x+;Rhl+b z;773uKfy<^jz1P|#q&5F(+b_%FTo`CH?8)FNo?xKv-)3*@L8Yt%mEx23{>P}- z=QGqv`>f$5yt2%_{LnXBEpm9%ihBMq4!~2W0KY)3+lP)GxXM5$jveLdMapZ2p_=gy zRP9!wz8{{(P&{IN8`VSaVG?#DNi#p7dMC5gJzyEClvSwfcb1ZWE%X2vw9#5rAiGdC zeIAwScX1H*3%DuGz(nS`s0AxfCz@lg&quvgYcU@8;uLI0?dKs)YUc1V@~?=8b3w0P z1*Tv4W@d=2&9zmI%*nx9Y^^70!<_bX8M=U@<)Rht>8?ruOW@Tk518Y<9ZsOSHODn%cv$rAZCQbv5J6PKW#n}E9CfcpGmJKu<- zm^+U%NM>*X^})|ksl0?b$*-skBvFTOOh>)fLs1zTiQ2ddmGYpSKZxpyt*DKkMFn&a z70`#sx{f)^KsEmg)A1rIHLiwLD+F&_qf%Vp{sOMfpE%gc2iC&1$Dq3sM797mHIfU>CZa$LNA7J!85^KI20q9C!r60=*3D5;@^<( z9P>IVGkx~HXQJEXX{dvgT7wwDd^PGd+<+>nvy;IC3|>ZcarPwFD%6JaP!F!dEPT@5 z??7ej6zXI@V*ukO+h5P9>nl*7Yel900OsL2q(qL1n&SSl$V5dp3)LixQ8j)Xb&?J{ zKaDZWuV6GrOm$0@fGSA<)r6Dmd=W-6UxzAni*+X^=>31mUO0*cJa8HZVE8oKOsEW$ zq5_cg4H7>*^>_L5B+*QkO*V`CuXW+wb9EiW5 zY8ExqeGUB>#r$^E#`ADCuA13ipFH=*_Rc(iOnX8g)sr*JU%h-m!^(wAm-uTMRxT*> o73LR~`11q)f+Amjpe(;QFTXH9zujN fallback sur toutes les équipes candidates + eligible_teams = candidate_teams + + if not eligible_teams: + return None, None + + # 4. Calendrier d'assignation + # Si aucune date n'est fournie, alors aucune date ne correspond -> ne pas assigner d'équipe + if not scheduled_date: + return None, None + + if hasattr(scheduled_date, 'date'): + target_d = scheduled_date.date() + else: + target_d = scheduled_date + + team_ids = {t.id for t in eligible_teams} + compositions = get_teams_composition_for_period(team_ids, target_d, target_d) + + all_member_ids = {m['id'] for m_list in compositions.values() for m in m_list} + absences = get_member_absences_for_period(all_member_ids, target_d, target_d) if all_member_ids else {} + + scored_teams = [] + for team in eligible_teams: + day_members = compositions.get((team.id, target_d), []) + available_members = [ + m for m in day_members + if (m['id'], target_d) not in absences + ] + scored_teams.append((len(available_members), team, available_members)) + + # Trier par nb de membres disponibles décroissant, puis nom de l'équipe + scored_teams.sort(key=lambda item: (-item[0], item[1].name)) + best_len, best_team, best_members = scored_teams[0] + + # Si aucune date ne correspond (aucun agent planifié/disponible à cette date), ne pas assigner d'équipe + if best_len == 0: + return None, None + + assigned_member = None + if best_members: + assigned_member = CompanyMember.objects.filter(id=best_members[0]['id']).first() + + return best_team, assigned_member + + def schedule_post_intervention_inspection(intervention): """ Planifie automatiquement une inspection de durabilité pour une intervention lorsque son sous-type requiert une inspection post-travaux (ex: Hot Box). """ - from .models import Inspection, InspectionZone + from .models import Inspection sub_type = getattr(intervention, 'sub_type', None) if not sub_type or not getattr(sub_type, 'requires_post_inspection', False): @@ -164,47 +282,41 @@ def schedule_post_intervention_inspection(intervention): scheduled_start_date = ref_date.date() + timedelta(days=min_days) scheduled_due_date = ref_date.date() + timedelta(days=max_days) - # Détermination de la zone d'inspection - thematic = intervention.thematic - zones = InspectionZone.objects.filter(thematic=thematic, is_active=True) if thematic else InspectionZone.objects.none() - zone = None + # Point pour résolution spatiale + point = None + if intervention.lon and intervention.lat: + try: + from django.contrib.gis.geos import Point + p = Point(float(intervention.lon), float(intervention.lat), srid=4326) + p.transform(3812) + point = p + except Exception as e: + logger.debug(f"Error transforming intervention coordinates to 3812: {e}") - # 1. Résolution spatiale par intersection / inclusion - if zones.exists(): - if intervention.geom: - zone = zones.filter(geom__intersects=intervention.geom).first() + # Récupérer les communes associées + munis = [] + from common.models import Municipality + for loc in intervention.locations.all(): + loc_obj = loc.location + muni_obj = loc_obj if isinstance(loc_obj, Municipality) else getattr(loc_obj, 'municipality', None) + if muni_obj: + munis.append(muni_obj) - if not zone and intervention.lon and intervention.lat: - try: - from django.contrib.gis.geos import Point - p = Point(float(intervention.lon), float(intervention.lat), srid=4326) - p.transform(3812) - zone = zones.filter(geom__contains=p).first() - except Exception as e: - logger.debug(f"Error resolving inspection zone via coordinates: {e}") + # Résolution automatique de l'équipe et du membre + assigned_team, assigned_member = resolve_team_and_member_for_inspection( + target_geom=intervention.geom, + point=point, + scheduled_date=scheduled_start_date, + thematic=intervention.thematic, + municipalities=munis, + ) - # 2. Résolution administrative par commune associée - if not zone: - from common.models import Municipality - for loc in intervention.locations.all(): - loc_obj = loc.location - muni_to_check = loc_obj if isinstance(loc_obj, Municipality) else getattr(loc_obj, 'municipality', None) - if muni_to_check: - zone = zones.filter(municipalities=muni_to_check).first() - if zone: - break + assigned_inspector = assigned_member.user if (assigned_member and assigned_member.user) else None # Détermination de la cible (Asset, Localisation ou Intervention elle-même) target_ct, target_id = _resolve_inspection_target(intervention, sub_type) - # Pré-assignation si un seul inspecteur est assigné à la zone - assigned_inspector = None - if zone: - zone_inspectors = list(zone.inspectors.all()[:2]) - if len(zone_inspectors) == 1: - assigned_inspector = zone_inspectors[0] - - from .models import Inspection, InspectionZone, HotBoxInspection + from .models import Inspection, HotBoxInspection is_hotbox = any(k in (sub_type.code or '').lower() or k in (sub_type.name_fr or '').lower() for k in ('hotbox', 'hot_box', 'hot box')) inspection_cls = HotBoxInspection if is_hotbox else Inspection @@ -220,9 +332,10 @@ def schedule_post_intervention_inspection(intervention): result_status='pending', scheduled_start_date=scheduled_start_date, scheduled_due_date=scheduled_due_date, - zone=zone, + assigned_team=assigned_team, + assigned_member=assigned_member, inspector=assigned_inspector, notes=notes, ) - logger.info(f"Scheduled post-intervention inspection {inspection.pk} created for intervention {intervention.code}") + logger.info(f"Scheduled post-intervention inspection {inspection.pk} created for intervention {intervention.code} (Team: {assigned_team})") return inspection diff --git a/loko/inspections/templates/inspections/components/edit_inspection_modal.html b/loko/inspections/templates/inspections/components/edit_inspection_modal.html index fd4e48b..34bbd83 100644 --- a/loko/inspections/templates/inspections/components/edit_inspection_modal.html +++ b/loko/inspections/templates/inspections/components/edit_inspection_modal.html @@ -672,8 +672,8 @@ document.addEventListener('DOMContentLoaded', function() { titleModal.innerHTML = `{% translate "Réaliser l'inspection" %}`; } let schedInfo = `{% translate "Fenêtre" %} : ${data.scheduled_start_date} → ${data.scheduled_due_date}`; - if (data.zone_name) { - schedInfo += ` | {% translate "Zone" %} : ${data.zone_name}`; + if (data.assigned_team_name || data.zone_name) { + schedInfo += ` | {% translate "Équipe" %} : ${data.assigned_team_name || data.zone_name}`; } if (data.checked_intervention_code) { schedInfo += ` | ${data.checked_intervention_code}`; diff --git a/loko/inspections/templates/inspections/inspections_list_mobile.html b/loko/inspections/templates/inspections/inspections_list_mobile.html index 2f9fe10..8cff4be 100644 --- a/loko/inspections/templates/inspections/inspections_list_mobile.html +++ b/loko/inspections/templates/inspections/inspections_list_mobile.html @@ -158,9 +158,9 @@ {{ insp.asset_content_type.name }} - {% if insp.zone %} + {% if insp.assigned_team %} - {{ insp.zone.name_fr }} + {{ insp.assigned_team.name }} {% endif %} @@ -304,10 +304,10 @@ {% translate "Fenêtre d'inspection" %} {{ insp.scheduled_start_date|date:"d/m/Y" }} → {{ insp.scheduled_due_date|date:"d/m/Y" }} - {% if insp.zone %} + {% if insp.assigned_team %}
  • - {% translate "District / Zone" %} - {{ insp.zone.name_fr }} + {% translate "Équipe assignée" %} + {{ insp.assigned_team.name }}
  • {% endif %} {% if insp.checked_intervention %} diff --git a/loko/inspections/tests.py b/loko/inspections/tests.py index 78004db..bdd163b 100644 --- a/loko/inspections/tests.py +++ b/loko/inspections/tests.py @@ -1288,7 +1288,7 @@ class ViewerInspectionPermissionsAndGeojsonTestCase(TestCase): # Number of queries should be bounded and independent of inspection count # (O(1) queries instead of O(N), unaffected by inspection count) - self.assertLessEqual(len(ctx), 28) + self.assertLessEqual(len(ctx), 32) class GiepInspectionStatusCalculationTestCase(TestCase): @@ -1574,41 +1574,118 @@ class PostInterventionDurabilityInspectionTestCase(TestCase): self.assertEqual(insp.timeliness, 'future') def test_zone_and_single_inspector_auto_assignment(self): - from common.models import Municipality - from interventions.models import InterventionLocation - from .models import InspectionZone + from datetime import timedelta + from contracts.models import Company, CompanyTeam, CompanyMember + from contracts.services_teams import set_member_team_schedule + from django.contrib.gis.geos import Polygon, MultiPolygon - zone = InspectionZone.objects.create( - code='DIST_NORD_TEST', - name_fr='District Nord Test', - thematic=self.roads_thematic + company = Company.objects.create(name="Voirie Nord Test") + poly = MultiPolygon(Polygon(((100000, 100000), (100000, 200000), (200000, 200000), (200000, 100000), (100000, 100000)))) + team = CompanyTeam.objects.create( + name="Inspection Zone 1 Test", + company=company, + geom=poly, + operation_types=['inspections'], ) - zone.inspectors.add(self.user) - muni = Municipality.objects.create(name_fr='Schaerbeek Test', name_nl='Schaarbeek Test', niscode='21099', fid='21099') - zone.municipalities.add(muni) + team.thematics.add(self.roads_thematic) + member = CompanyMember.objects.create(name="Inspecteur Alice", user=self.user) + + target_date = (timezone.now() + timedelta(days=30)).date() + set_member_team_schedule(member, team.id, target_date) intervention = Intervention.objects.create( thematic=self.roads_thematic, type='repair', sub_type=self.hotbox_subtype, status='in_progress', + geom=poly, end_time=timezone.now() ) - InterventionLocation.objects.create( - intervention=intervention, - content_type=ContentType.objects.get_for_model(muni), - object_id=muni.pk - ) intervention.status = 'finished' intervention.save() insp = Inspection.objects.get(checked_intervention=intervention) - self.assertEqual(insp.zone, zone) - # Inspecteur unique pré-assigné + self.assertEqual(insp.assigned_team, team) + self.assertEqual(insp.assigned_member, member) + # Inspecteur unique pré-assigné via le membre self.assertEqual(insp.inspector, self.user) self.assertTrue(insp.can_edit(self.user)) + def test_auto_assignment_fallback_calendar_when_no_geometry(self): + """Si aucune géométrie n'est définie, fallback sur le calendrier d'assignation.""" + from datetime import timedelta + from contracts.models import Company, CompanyTeam, CompanyMember + from contracts.services_teams import set_member_team_schedule + + company = Company.objects.create(name="Voirie Fallback Test") + team = CompanyTeam.objects.create( + name="Équipe Sans Zone", + company=company, + geom=None, + operation_types=['inspections'], + ) + team.thematics.add(self.roads_thematic) + member = CompanyMember.objects.create(name="Inspecteur Bob", user=self.user) + + target_date = (timezone.now() + timedelta(days=30)).date() + set_member_team_schedule(member, team.id, target_date) + + # Intervention SANS géométrie + intervention = Intervention.objects.create( + thematic=self.roads_thematic, + type='repair', + sub_type=self.hotbox_subtype, + status='in_progress', + geom=None, + lon=None, + lat=None, + end_time=timezone.now() + ) + intervention.status = 'finished' + intervention.save() + + insp = Inspection.objects.get(checked_intervention=intervention) + self.assertEqual(insp.assigned_team, team) + self.assertEqual(insp.assigned_member, member) + self.assertEqual(insp.inspector, self.user) + + def test_auto_assignment_no_team_assigned_when_no_date_matches(self): + """Si aucune date ne correspond (aucun agent planifié ce jour-là), ne pas assigner d'équipe.""" + from datetime import timedelta + from contracts.models import Company, CompanyTeam, CompanyMember + from contracts.services_teams import set_member_team_schedule + + company = Company.objects.create(name="Voirie No Date Test") + team = CompanyTeam.objects.create( + name="Équipe Dispo Uniquement Demain", + company=company, + geom=None, + operation_types=['inspections'], + ) + team.thematics.add(self.roads_thematic) + member = CompanyMember.objects.create(name="Inspecteur Charlie", user=self.user) + + # Planifié demain, mais l'inspection a lieu dans 30 jours + tomorrow = (timezone.now() + timedelta(days=1)).date() + set_member_team_schedule(member, team.id, tomorrow) + + intervention = Intervention.objects.create( + thematic=self.roads_thematic, + type='repair', + sub_type=self.hotbox_subtype, + status='in_progress', + geom=None, + end_time=timezone.now() + ) + intervention.status = 'finished' + intervention.save() + + insp = Inspection.objects.get(checked_intervention=intervention) + # Aucune date ne correspond dans 30 jours -> pas d'équipe assignée + self.assertIsNone(insp.assigned_team) + self.assertIsNone(insp.assigned_member) + def test_non_compliance_triggers_team_notification(self): from notifications.models import NotificationRecipient intervention = Intervention.objects.create( diff --git a/loko/inspections/views.py b/loko/inspections/views.py index c023b4e..85c25e3 100644 --- a/loko/inspections/views.py +++ b/loko/inspections/views.py @@ -11,8 +11,9 @@ import json from common.models import Thematic from common.utils import get_short_name from assets.models import AssetCategory +from contracts.models import CompanyTeam from .models import ( - Inspection, InspectionZone, ParkingSpotInspection, GiepInspection, SignPanelInspection, HotBoxInspection, InspectionDocument, + Inspection, ParkingSpotInspection, GiepInspection, SignPanelInspection, HotBoxInspection, InspectionDocument, INSPECTION_RESULT_CHOICES, INSPECTION_NATURE_CHOICES, INSPECTOR_ROLE_CHOICES ) @@ -45,7 +46,8 @@ def _filter_inspections_queryset(request): qs = filter_viewable_inspections_for_user(request.user) qs = qs.select_related( 'asset_content_type', 'inspector', 'mission_intervention', 'checked_intervention', - 'parkingspotinspection', 'giepinspection', 'signpanelinspection' + 'parkingspotinspection', 'giepinspection', 'signpanelinspection', + 'assigned_team', 'assigned_member' ).prefetch_related('documents') thematics = request.GET.getlist('thematic') @@ -159,16 +161,16 @@ def _filter_inspections_queryset(request): if natures: qs = qs.filter(nature__in=natures) - zone_id = request.GET.get('zone') or request.GET.get('zone_id') - if zone_id: - qs = qs.filter(zone_id=zone_id) + team_filter = request.GET.get('team') or request.GET.get('team_id') or request.GET.get('zone') or request.GET.get('zone_id') + if team_filter: + qs = qs.filter(assigned_team_id=team_filter) show_mine_only = request.GET.get('mine') == '1' or request.GET.get('tab') == 'my' if show_mine_only: qs = qs.filter(inspector=request.user) - if request.GET.get('mine_zones') == '1': - qs = qs.filter(Q(zone__inspectors=request.user) | Q(inspector=request.user)) + if request.GET.get('mine_zones') == '1' or request.GET.get('mine_teams') == '1': + qs = qs.filter(Q(assigned_team__members__user=request.user) | Q(inspector=request.user)) timeliness = request.GET.get('timeliness') if timeliness: @@ -224,7 +226,7 @@ def inspections_list(request): # Compteurs rapides pour les onglets scheduled_count = base_viewable.filter(status='scheduled').filter( - Q(inspector=request.user) | Q(zone__inspectors=request.user) | Q(inspector__isnull=True) + Q(inspector=request.user) | Q(assigned_team__members__user=request.user) | Q(inspector__isnull=True) ).count() my_count = base_viewable.filter(inspector=request.user).exclude(status='scheduled').count() @@ -233,20 +235,20 @@ def inspections_list(request): thematic_code = request.GET.get('thematic', '') status_filter = request.GET.get('status', '') nature_filter = request.GET.get('nature', '') - zone_filter = request.GET.get('zone', '') + zone_filter = request.GET.get('team', '') or request.GET.get('zone', '') mission_id = request.GET.get('mission_id', '') checked_interv_id = request.GET.get('checked_interv_id', '') search_q = request.GET.get('q', '').strip() qs = base_viewable.select_related( 'asset_content_type', 'inspector', 'mission_intervention', 'checked_intervention', - 'zone', 'parkingspotinspection', 'giepinspection', 'signpanelinspection' + 'assigned_team', 'assigned_member', 'parkingspotinspection', 'giepinspection', 'signpanelinspection' ).prefetch_related('documents') if filter_type == 'scheduled': qs = qs.filter(status='scheduled') if request.GET.get('all_zones') != '1': - qs = qs.filter(Q(inspector=request.user) | Q(zone__inspectors=request.user) | Q(inspector__isnull=True)) + qs = qs.filter(Q(inspector=request.user) | Q(assigned_team__members__user=request.user) | Q(inspector__isnull=True)) qs = qs.order_by('scheduled_due_date', 'scheduled_start_date', '-id') elif filter_type == 'my': qs = qs.filter(inspector=request.user).exclude(status='scheduled').order_by('-inspection_date', '-id') @@ -254,7 +256,7 @@ def inspections_list(request): qs = qs.order_by('-inspection_date', '-id') if zone_filter: - qs = qs.filter(zone_id=zone_filter) + qs = qs.filter(assigned_team_id=zone_filter) if thematic_code: try: @@ -327,9 +329,8 @@ def inspections_list(request): qs = qs.filter(search_filter) - zones = InspectionZone.objects.filter(is_active=True).order_by('thematic__name_fr', 'name_fr') - if thematics.exists(): - zones = zones.filter(thematic__in=thematics) + all_teams = CompanyTeam.objects.filter(geom__isnull=False).select_related('company').order_by('name') + zones = [t for t in all_teams if t.handles_operation(CompanyTeam.OPERATION_INSPECTIONS)] context = { 'base_template': "mobile/mobile_base.html", @@ -403,9 +404,8 @@ def inspections_list(request): for cfg in result_choices_config ] - zones = InspectionZone.objects.filter(is_active=True).order_by('thematic__name_fr', 'name_fr') - if thematics.exists(): - zones = zones.filter(thematic__in=thematics) + all_teams = CompanyTeam.objects.filter(geom__isnull=False).select_related('company').order_by('name') + zones = [t for t in all_teams if t.handles_operation(CompanyTeam.OPERATION_INSPECTIONS)] context = { 'thematic_choices': [(t.code, t.name, t.icon or 'bi-geo-alt') for t in thematics], @@ -599,6 +599,20 @@ def create_inspection_api(request): }, status=400) try: + # Résolution de l'équipe assignée et du membre si non spécifiés + assigned_team = None + assigned_member = None + team_id_raw = request.POST.get('assigned_team_id') or request.POST.get('team_id') + if team_id_raw: + assigned_team = CompanyTeam.objects.filter(pk=team_id_raw).first() + if not assigned_team: + from .services import resolve_team_and_member_for_inspection + target_geom = getattr(target_obj, 'geom', None) + assigned_team, assigned_member = resolve_team_and_member_for_inspection( + target_geom=target_geom, + scheduled_date=timezone.now().date(), + ) + if is_parking_spot: inspection = ParkingSpotInspection( asset_content_type=content_type, @@ -613,6 +627,8 @@ def create_inspection_api(request): verified_obsolescence=verified_obsolescence, notes=notes, apply_changes_to_asset=apply_changes, + assigned_team=assigned_team, + assigned_member=assigned_member, ) if 'verified_standard_stand_count' in request.POST and request.POST['verified_standard_stand_count'] != '': @@ -665,6 +681,8 @@ def create_inspection_api(request): date_derniere_pluie=date_derniere_pluie, orage_moins_une_heure=orage_moins_une_heure, data=parsed_giep_data, + assigned_team=assigned_team, + assigned_member=assigned_member, ) inspection.save() elif is_signpanel: @@ -726,7 +744,10 @@ def create_inspection_api(request): ai_detected_code=ai_code, ai_confidence=ai_conf, apply_sign_updates_to_asset=apply_changes, + assigned_team=assigned_team, + assigned_member=assigned_member, ) + inspection.save() elif is_hotbox: joint_level = request.POST.get('joint_level') or None joint_sealing = request.POST.get('joint_sealing') or None @@ -756,6 +777,8 @@ def create_inspection_api(request): joint_sealing=joint_sealing, cracking=cracking, water_drainage=water_drainage, + assigned_team=assigned_team, + assigned_member=assigned_member, ) inspection.save() else: @@ -772,6 +795,8 @@ def create_inspection_api(request): verified_obsolescence=verified_obsolescence, notes=notes, apply_changes_to_asset=apply_changes, + assigned_team=assigned_team, + assigned_member=assigned_member, ) # Enregistrement des photos et documents joints @@ -898,8 +923,10 @@ def get_inspection_detail_api(request, inspection_id): 'is_scheduled': leaf.is_scheduled, 'scheduled_start_date': leaf.scheduled_start_date.strftime('%d/%m/%Y') if leaf.scheduled_start_date else '', 'scheduled_due_date': leaf.scheduled_due_date.strftime('%d/%m/%Y') if leaf.scheduled_due_date else '', - 'zone_id': leaf.zone_id, - 'zone_name': leaf.zone.get_name() if leaf.zone else '', + 'assigned_team_id': leaf.assigned_team_id, + 'assigned_team_name': leaf.assigned_team.name if leaf.assigned_team else '', + 'zone_id': leaf.assigned_team_id, + 'zone_name': leaf.assigned_team.name if leaf.assigned_team else '', 'timeliness': leaf.timeliness, 'timeliness_label': str(leaf.timeliness_label) if leaf.timeliness_label else '', 'timeliness_badge_class': leaf.timeliness_badge_class, diff --git a/loko/sign/migrations/0005_alter_signpole_support_type.py b/loko/sign/migrations/0005_alter_signpole_support_type.py new file mode 100644 index 0000000..0adfd2c --- /dev/null +++ b/loko/sign/migrations/0005_alter_signpole_support_type.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.7 on 2026-09-19 08:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sign', '0004_signpanel_support_model_signpanel_trafficlight_pole_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='signpole', + name='support_type', + field=models.CharField(blank=True, choices=[('pole', 'Poteau'), ('lightingpole', "Poteau d'éclairage"), ('trafficlightpole', 'Poteau de signalisation lumineuse'), ('facade', 'Façade'), ('mast', 'Mât'), ('portico', 'Portique'), ('gallows', 'Potence'), ('hopper', 'Trémie'), ('other', 'Autre')], default='pole', max_length=50, verbose_name='Type de support'), + ), + ]