DavidD003 commited on
Commit
db6bc35
1 Parent(s): 930eac7

Upload SchedBuilderClasses2.py

Browse files
Files changed (1) hide show
  1. SchedBuilderClasses2.py +1039 -0
SchedBuilderClasses2.py ADDED
@@ -0,0 +1,1039 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from copy import deepcopy
2
+ from ctypes.wintypes import WPARAM
3
+ import openpyxl as pyxl
4
+ import functools
5
+ import SchedBuilderUtyModule as tls
6
+ # from importlib import reload
7
+
8
+ def debug(func):
9
+ """Print the function signature and return value"""
10
+ @functools.wraps(func)
11
+ def wrapper_debug(*args, **kwargs):
12
+ args_repr = [repr(a) for a in args] # 1
13
+ kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] # 2
14
+ signature = ", ".join(args_repr + kwargs_repr) # 3
15
+ print(f"Calling {func.__name__}({signature})")
16
+ value = func(*args, **kwargs)
17
+ print(f"{func.__name__!r} returned {value!r}") # 4
18
+ return value
19
+ return wrapper_debug
20
+
21
+
22
+ class Slot():
23
+ """A single 4 hour time slot for a single job, to be filled by 1 person"""
24
+ def __init__(self,seqID,dispNm,trnNm=None):
25
+ self.trnNm=trnNm #to be used when filtering out staff for training
26
+ self.dispNm=dispNm #to be used for printouts
27
+ self.seqID=seqID
28
+ #self.datetime= #Determined based on seq. Used in printout of assignments
29
+ self.assignee=None #To store eeid of assignee for printout
30
+ self.assnType=None #e.g. Forced/WWF
31
+ self.slotInShift=0 #1 if first slot in someones shift. 2 if second, etc.
32
+ self.totSlotsInShift=0 # 1 if 4 hours shift, 2 if 8 hour shift, 3 if 12 hour shift
33
+ self.eligVol=0 #This will be used to track which slot is the most constrained...it tracks the count of how many people eligible volunteers this slot has going for it
34
+ self.disallowed=[] #EEID's that were specified as not allowed to be assigned to this slot in the Assn List
35
+
36
+ def key(self):
37
+ return str(self.seqID)+'_'+self.dispNm
38
+
39
+ def assn(self,sch,assnType=None,slAssignee=None,fromList=False):
40
+ """Assign a slot to someone, and perform associated variable tracking etc. Returns a bool indicating if a forcing rule was broken or not"""
41
+ if (slAssignee is not None) and assnType=='DNS': #Case that this is specifying *not* to assign someone. In every other case it is a matter of actually assigning someone
42
+ self.disallowed.append(slAssignee)
43
+ else:
44
+ self.assnType=assnType
45
+ self.assignee=slAssignee #eeid
46
+ if assnType in ['WWF','F','V']:
47
+ pass
48
+ # del sch.slots[self.key()] #Remove this slot from the 'openslots' collection if someone was actually assigned
49
+ # #del sch.slots[self.key()]
50
+ # del sch.fslots[self.key()]
51
+ elif assnType=='nV':
52
+ pass
53
+ #del sch.slots[self.key()]
54
+ if slAssignee is not None: #Case of specific assignment, only not follwoed through when its no ee and DNS
55
+ sch.ee[slAssignee].assnBookKeeping(self,sch) #add this slot to the ee's assigned slot dictionary & other tasks
56
+ #Logging for printout after
57
+ if assnType!=None and slAssignee!=None:
58
+ logTxt=''
59
+ if fromList==True:
60
+ logTxt+= 'Per Assn List: '
61
+ if assnType=='DNS':
62
+ logTxt+='Removed slot '+self.dispNm+' '+ sch.slLeg[self.seqID-1][2]+' ('+sch.slLeg[self.seqID-1][1]+') from scheduling'
63
+ if slAssignee is not None: logTxt+= ' for ee '+ sch.ee[slAssignee].firstNm[0]+'. '+sch.ee[slAssignee].lastNm
64
+ elif assnType=='WWF': logTxt+="WWF Assignment: "+sch.ee[slAssignee].firstNm[0]+'. '+sch.ee[slAssignee].lastNm+' to ' +self.dispNm+' '+ sch.slLeg[self.seqID-1][2]+' ('+sch.slLeg[self.seqID-1][1]+')'
65
+ elif assnType=='F': logTxt+=" FORCED Assignment: "+ sch.ee[slAssignee].firstNm[0]+'. '+sch.ee[slAssignee].lastNm+ ' to ' +self.dispNm+' '+ sch.slLeg[self.seqID-1][2]+' ('+sch.slLeg[self.seqID-1][1]+')'
66
+ elif assnType=='V': logTxt+=" Voluntary Assignment: "+ sch.ee[slAssignee].firstNm[0]+'. '+sch.ee[slAssignee].lastNm+' to ' +self.dispNm+' '+ sch.slLeg[self.seqID-1][2]+' ('+sch.slLeg[self.seqID-1][1]+')'
67
+ elif assnType=='N':logTxt+=" No voluntary or forced assignment could be made to "+self.dispNm+' '+ sch.slLeg[self.seqID-1][2]+' ('+sch.slLeg[self.seqID-1][1]+')'
68
+ elif assnType=='nV':logTxt+=" No voluntary assignment could be made to "+self.dispNm+' '+ sch.slLeg[self.seqID-1][2]+' ('+sch.slLeg[self.seqID-1][1]+')'
69
+ sch.assnLog.append(logTxt)
70
+ if slAssignee!=None and assnType in ['V','F']: #Check if assignment breaks forcing rules
71
+ if sch.ee[slAssignee].frcOK(sch)!=True: #Case that the assignment broke a forcing rule... that slot needs be earlier priority.
72
+ return False
73
+ return True
74
+
75
+ class ee():
76
+ """A staff persons data as related to weekend scheduling"""
77
+ def __init__(self,snty,crew,id,Last,First,refHrs,wkHrs,wkndHrs=0,skills=[]):
78
+ self.seniority=int(snty)
79
+ if refHrs==None:
80
+ self.refHrs=0
81
+ else:
82
+ self.refHrs=float(refHrs)
83
+ if crew=='wwf':self.wkdyHrs=0
84
+ else: self.wkdyHrs=float(wkHrs)
85
+ self.wkndHrs=float(wkndHrs)
86
+ self.frcHrs=0
87
+ self.lastNm=Last
88
+ self.firstNm=First
89
+ self.eeID=int(id)
90
+ self.crew=crew
91
+ self.assignments=[]#To be appended with slots as they are assigned, keyed as they are in the slot dictionary
92
+ self.skills=skills
93
+
94
+ def dispNm(self,slt=None):
95
+ if slt is None:
96
+ if int(self.seniority)>50000: return self.firstNm[0]+'.'+self.lastNm[0]+self.lastNm[1:].lower()+'(T)' #Case of Temps
97
+ else: return (self.firstNm[0]+'.'+self.lastNm[0]+self.lastNm[1:].lower()).replace(' ','-')
98
+ # elif : #Can make functionality to pass in 'MESR' to display name based on some slot criteria?
99
+ # pass
100
+ elif slt=='read':
101
+ return (self.firstNm[0]+'.'+self.lastNm[0]+self.lastNm[1:].lower()).replace(' ','-')
102
+
103
+ def frcOK(self,sch):
104
+ """Returns if the present assignments are permissible with rules around forcing limitations"""
105
+ #Used after making assignments to check if need make a slot priority or not
106
+ asn=sorted(self.assignments,key=lambda k:int(k[:k.index('_')])) #Order assignment keys by their slot ID's
107
+ h=self.wkdyHrs #initialize tally
108
+ for k in asn: #For each slot, considering the hours worked upon completion of that workslot
109
+ h+=4
110
+ if h>48 and sch.slots[k].assnType=='F': return False #Forcing rules broken, if condition is met then forcing past 48 hours in week
111
+ return True #if the above condition not met.. all good
112
+
113
+ def frcOKdblAssn(self,sch,sl):
114
+ """Acts as if 2 slots are to be assigned (phase 1v3) and checks if previously made forcing,sched'd later in time, not ok with that"""
115
+ #Used after making assignments to check if need make a slot priority or not
116
+ if self.frcHrs==0: return True
117
+ asn=sorted(self.assignments,key=lambda k:int(k[:k.index('_')])) #Order assignment keys by their slot ID's
118
+ h=self.wkdyHrs #initialize tally
119
+ c=0 #Tracks if the extra 8 have already been applied
120
+ for k in asn: #For each slot, considering the hours worked upon completion of that workslot
121
+ h+=4
122
+ if c==0 and int(k[:k.index("_")])>sl.seqID: #Branch taken first time seeing a slot later in time than the 8 being tested
123
+ h+=8 #Pretend 2 slots assigned
124
+ c=1 #So this doesn't keep getting reapplied every time moving forward
125
+ if h>48 and sch.slots[k].assnType=='F': return False #Forcing rules broken, if condition is met then forcing past 48 hours in week
126
+ return True #if the above condition not met.. all good
127
+
128
+ def assnBookKeeping(self,sl,sch):
129
+ """Carried out when assigned to slot, adjusts tally of eligible volunteers to other slots accordingly"""
130
+ self.assignments.append(sl.key())
131
+ if sl.assnType=='F': self.frcHrs+=4 #Track forced hours
132
+ if sl.assnType!='WWF': self.wkndHrs+=4 #Don't track wwf hours as weekend hours because then it goes over 60 and they dont get any voluntary OT
133
+ # Return keys for slots that the person was counted as an eligible volunteer for
134
+ kys=[k for k in sch.slots if self.eeID in sch.slots[k].eligVol]
135
+ for k in kys:
136
+ if self.slOK(sch,sch.slots[k],pt=False,poll=tls.viewTBL('allPollData',filterOn=[('eeid',self.eeID)])[0]) is not True: sch.slots[k].eligVol.pop(sch.slots[k].eligVol.index(self.eeID))
137
+ #pop the eeId out of eligVol list if the given slot is no longer ok to assign
138
+ #The slOK function does not capture if making a voluntary assignment earlier in the weekend invaldiates a forced assignment later in the weekend.
139
+ #That is captured in a separate function 'frcOK'
140
+
141
+
142
+ def totShiftHrs(self,sl,toFlw=False,styling=False):
143
+ """Given a slot, assuming it is assigned, what is the total shift length of the shift in which that slot is a constituent. If toFlw=True then return # slots to follow present slot in same shift"""
144
+ sLen=1 #start off shift length at one because the slot being passed in is always minimum
145
+ assnSeqIDs=[int(k[:k.index('_')]) for k in self.assignments] #Pull out the seqID's from the key strings for each slot an ee is already assigned
146
+ if len(assnSeqIDs)==0:
147
+ if toFlw==False:
148
+ return 4 #Case that no slots assigned so far, so if sl assigned then its alone
149
+ else: return 0 #single shift, no following
150
+ else:
151
+ if toFlw==False:
152
+ anch=sl.seqID
153
+ assnSeqIDs.append(anch)
154
+ assnSeqIDs.sort() #sorts integers lowest to highest
155
+ if styling==True: #Coutning method is simpler when just counting shift length for schedule colouring, need not account for slot passed in being potential 4slot consectuvie work slot
156
+ for offset in [-2,-1,1,2]: #Knowing that shifts will never be assigned 4 in a row, and there won't be a 4 hour gap between shifts, just count neighbouring 4 slots (2 on each side)
157
+ if anch+offset in assnSeqIDs:
158
+ sLen+=1
159
+ return sLen*4 #Return num hours
160
+ else: #Originally was using the same method as styling, with limits as -3,+3, but made a bug.. even if the two neighbouring slots on 1 side were unassigned (8hr gap), it would still count the 3rd if it was assigned, even though it shouldnt be counted as not a contiguous shift
161
+ assnSeqIDs=list(set(assnSeqIDs)) #converting to set eliminates duplicates ( in event slot being passed in was already assigned.. it was appended again)
162
+ def ranges(nums):
163
+ """Returns a list of (open,close) intervals a list spans"""
164
+ nums = sorted(set(nums))
165
+ gaps = [[s, e] for s, e in zip(nums, nums[1:]) if s+1 < e]
166
+ edges = iter(nums[:1] + sum(gaps, []) + nums[-1:])
167
+ return list(zip(edges, edges))
168
+ rns=ranges(assnSeqIDs) #dont have to worry that following line failes to index list comp @[0] because the slot in question is always passed in, even if no other assignments, the slot in question will be returned as 1 slot 4 hours
169
+ myRn=[x for x in rns if x[0]<=anch and x[1]>=anch][0] #Pull out a tuple contianing lower and upper bound of sequence of seqID's of which the slot in question is a part
170
+ return (myRn[1]-myRn[0]+1)*4 #e.g. range is seqIDs (6,7), thats two slots. 7-6+1=2
171
+ else: #Count # of slots that follow this one consecutively
172
+ i=0 #Count of slots to follow on same shift
173
+ for offset in [1,2]: #Never more than 3 in a row so need only check the 2 following slot seqID's for assignment
174
+ if anch+offset in assnSeqIDs:
175
+ i+=1
176
+ return i
177
+
178
+ def assnConflict(self,sl):
179
+ """Returns true if someone is already assigned to a slot with same seqID as potential assignment, false if no conflict"""
180
+ assns=[int(k[:k.index('_')]) for k in self.assignments] #Pull out the seqID's form the key strings for each slot an ee is already assigned
181
+ if len(assns)==0:return False #if no other assignments.. no conflict
182
+ elif sl.seqID not in assns: return False #if no other assns with same seqID.. no conflict
183
+ elif sl.seqID in assns: return True #if other assignment already amde with same seqID... true, conflict present
184
+
185
+
186
+ def gapOK(self,sl,sch,tp='V'):
187
+ """Returns true if the slot, when assigned, doesn't break the rule for minimum gap between shifts. 12 hours for forcing, 8 for vol"""
188
+ assns=[int(k[:k.index('_')]) for k in self.assignments] #Pull out the seqID's form the key strings for each slot an ee is already assigned
189
+ #Just need to check the nearest neighbours aren't with a gap of 1 empty slot
190
+ deltaNextShifts=[v for v in [sId-sl.seqID for sId in assns] if v>0] # Define this and the next before defining next/prevNeighDist to manage case of 0 this being equal to [], which min() can't accept
191
+ deltaPrevShifts=[v for v in [sl.seqID-sId for sId in assns] if v>0] ##Basically, subtract various Id's from sl in question. Remove vlaues<0 bc those slots come later (next neighb). Then take the minimum value, which is the diff in seqID between sl being compared, and nearest neighbour on left when sorted chronologically
192
+ if len(deltaNextShifts)==0:
193
+ deltaNextShifts=[0] #Fill with bogus # to give 'ok' result to function
194
+ if len(deltaPrevShifts)==0:
195
+ deltaPrevShifts=[0]
196
+ nextNeighbDist=min(deltaNextShifts)
197
+ prevNeighbDist=min(deltaPrevShifts)
198
+ #Utility functions:
199
+ def okForLastWkShift():
200
+ if self.crew==sch.Bcrew and sl.seqID-6*(sch.friOT==False)<3+1*(tp=='F'):
201
+ return False #Case of B shift worker being assigned within 8 (or 12 if forced) hrs of last weeks final afternoon shift, no go
202
+ elif self.crew==sch.Acrew and tp=='F' and sl.seqID-6*(sch.friOT==False)<2:
203
+ return False #Case of A shift worker being forced onto first slot of the weekend
204
+ else: return True
205
+ def okForNextWkShift():
206
+ if self.crew=='rock' and tp=='V' and sl.seqID+6*(sch.monOT==False)==23:
207
+ return False #Case of night shift person being assigned 3p-7p afternoon before weekday night shift
208
+ elif self.crew=='rock' and tp=='F' and sl.seqID+6*(sch.monOT==False)>21:
209
+ return False #Case of night shift forcing.. can't be forced such that <12 hours before next shift
210
+ elif self.crew==sch.Acrew and tp=='F' and sl.seqID+6*(sch.monOT==False)==24:
211
+ return False #Case of day shift ee (next week) being forced 7p-11p the night before
212
+ else: return True
213
+ #=====
214
+ if tp=='V' and (nextNeighbDist==2 or prevNeighbDist==2):
215
+ return False#Distance of one means consecutive slots (shiftlength already checked), distance greater than 2 is gap of 8 hours or more
216
+ elif tp=='F' and ((nextNeighbDist==2 or prevNeighbDist==2) or prevNeighbDist==3):return False #Forcing requires gap of 12h or more going into it, 8 hours after it
217
+ elif okForLastWkShift()==True and okForNextWkShift()==True: return True #Reaching this elif means that the other conditions aren't true, so lastly just have to check the gap with weekday shifts ok
218
+ else: return False #Some condition not met
219
+
220
+ def slOK(self,sch,sl,poll=0,tp='V',pt=True): #pt is 'print', pVol is 'print did not volunteer' or not
221
+ """Returns True if the slot being tested is ok to be assigned, false if not"""
222
+ #Test all conditions (trained, wk hrs, consec shift, time between shifts, before making a branch to test willingness or not based on assignment type forced/voluntary)
223
+ if sl.assignee==None:#Finding people assigned to WWF slots for some reason.. this fixed that category of problem.
224
+ if (sl.dispNm in self.skills) or sl.trnNm=='ODD JOB': #the person is trained and hasn't been specified in assignment log *not* to be assigned here
225
+ if (self.eeID not in sl.disallowed): #this ee/slot pairing isn't ruled out in disallowment list
226
+ if (self.wkndHrs+self.wkdyHrs<60 and sl.seqID<19) or sl.seqID>18: #total week hours ok!
227
+ if self.assnConflict(sl)==False: #No existing assignment at same time!
228
+ if self.totShiftHrs(sl)<=12: #This slot wouldn't have a given shift exceed 12 hours
229
+ if self.gapOK(sl,sch,tp=tp): #This slot being assigned doesn't break a shift gap rule
230
+ if self.crew in ['wwf','bud','blue','rock','silver','gold','student']:
231
+ if tp=='V':#voluntary: check willigness
232
+ if (poll[3+sl.seqID] !="") and (poll[3+sl.seqID] is not None) and (poll[3+sl.seqID]!='n'): #Person is willing!
233
+ if self.lastNm=='Bruno': sch.assnLog.append(self.lastNm+' found ok for sl'+sl.key())
234
+ return True
235
+ else:
236
+ if sch.sF==False and pt==True and sch.pVol==True: sch.assnLog.append(' Fail to assign '+self.firstNm[0]+'. '+self.lastNm+' to ' +sl.dispNm+ ' '+sch.slLeg[sl.seqID-1][1]+' '+sch.slLeg[sl.seqID-1][2]+' || Did not volunteer for this shift')
237
+ return False
238
+ else:
239
+ if (self.wkndHrs+self.wkdyHrs<48) and (self.crew in ['bud','blue','rock','student']): return True #Forced
240
+ elif (sch.sF==False and pt==True) and (self.crew in ['bud','blue','rock','student']): sch.assnLog.append(' Fail to assign '+self.firstNm[0]+'. '+self.lastNm+' to ' +sl.dispNm+ ' '+sch.slLeg[sl.seqID-1][1]+' '+sch.slLeg[sl.seqID-1][2]+' || Cannot force past 48 hours worked in week')
241
+ elif (sch.sF==False and pt==True) and (self.crew in ['wwf','silver','gold']): sch.assnLog.append(' Fail to assign '+self.firstNm[0]+'. '+self.lastNm+' to ' +sl.dispNm+ ' '+sch.slLeg[sl.seqID-1][1]+' '+sch.slLeg[sl.seqID-1][2]+' || Cannot force a WWF person')
242
+ elif sch.sF==False and pt==True: sch.assnLog.append(' Fail to assign '+self.firstNm[0]+'. '+self.lastNm+' to ' +sl.dispNm+ ' '+sch.slLeg[sl.seqID-1][1]+' '+sch.slLeg[sl.seqID-1][2]+' || Crew is not in list wwf,bud,blue,rock,silver,gold,student')
243
+ elif sch.sF==False and pt==True: sch.assnLog.append(' Fail to assign '+self.firstNm[0]+'. '+self.lastNm+' to ' +sl.dispNm+ ' '+sch.slLeg[sl.seqID-1][1]+' '+sch.slLeg[sl.seqID-1][2]+' || Insufficient gap time between this shift and another')
244
+ elif sch.sF==False and pt==True: sch.assnLog.append(' Fail to assign '+self.firstNm[0]+'. '+self.lastNm+' to ' +sl.dispNm+ ' '+sch.slLeg[sl.seqID-1][1]+' '+sch.slLeg[sl.seqID-1][2]+' || Total consecutive hours would exceed 12')
245
+ elif sch.sF==False and pt==True: sch.assnLog.append(' Fail to assign '+self.firstNm[0]+'. '+self.lastNm+' to ' +sl.dispNm+ ' '+sch.slLeg[sl.seqID-1][1]+' '+sch.slLeg[sl.seqID-1][2]+' || Is already assigned for slot in same time period')
246
+ elif sch.sF==False and pt==True: sch.assnLog.append(' Fail to assign '+self.firstNm[0]+'. '+self.lastNm+' to ' +sl.dispNm+ ' '+sch.slLeg[sl.seqID-1][1]+' '+sch.slLeg[sl.seqID-1][2]+' || Total hours in week exceeds 60')
247
+ elif sch.sF==False and pt==True: sch.assnLog.append(' Fail to assign '+self.firstNm[0]+'. '+self.lastNm+' to ' +sl.dispNm+ ' '+sch.slLeg[sl.seqID-1][1]+' '+sch.slLeg[sl.seqID-1][2]+' || Disallowed in Assignment List')
248
+ elif sch.sF==False and sch.pNT==True and pt==True: sch.assnLog.append(' Fail to assign '+self.firstNm[0]+'. '+self.lastNm+' to ' +sl.dispNm+ ' '+sch.slLeg[sl.seqID-1][1]+' '+sch.slLeg[sl.seqID-1][2]+' || Not Trained')
249
+ elif sch.sF==False and sch.pNT==True and pt==True: sch.assnLog.append(' Fail to assign '+self.firstNm[0]+'. '+self.lastNm+' to ' +sl.dispNm+ ' '+sch.slLeg[sl.seqID-1][1]+' '+sch.slLeg[sl.seqID-1][2]+' || Slot already assigned')
250
+ return False
251
+
252
+ class Schedule():
253
+ def __init__(self,Acrew,slots,ee,preAssn,senList,polling,slLeg,sF=False,pNT=False,assnWWF=False,pVol=False,xtraDays=None,maxI=100):
254
+ # self.ftInfoTbl=ftInfoTbl
255
+ self.xtraDays=xtraDays #Selected on Gradio interface. a list of Monday and or Friday if those are to be scheduled. Used obly for gapOK funciton
256
+ if 'Friday' in xtraDays: self.friOT=True
257
+ else: self.friOT=False
258
+ if 'Monday' in xtraDays: self.monOT=True
259
+ else: self.monOT=False
260
+ self.pVol=pVol
261
+ self.assnWWF=assnWWF #this boolean indicates if we will be slotting WWF workers into the schedule according to their polling or not. This would be set to 'True' on a long weekend. Whenever it is set to false, the expectation would be that the WWF are fully assigned via assertions in the template file.
262
+ self.pNT=pNT #if True prints '...Not Trained'' statement as applicable when testing if a given slot is ok for someone
263
+ self.sF=sF #suppressFails.. if false, prints out 'failed to schedule' statements
264
+ self.Acrew=Acrew
265
+ if Acrew=='Bud':self.Bcrew='Blue'
266
+ else:self.Bcrew='Bud'
267
+ self.slots= slots #A collection of Slot objects that compose this schedule
268
+ # self.slots= deepcopy(slots) #Referenced in forcing phase 1
269
+ # self.slots=deepcopy(slots) #Referenced in voluntary assignment phase
270
+ # self.fslots=deepcopy(slots) #Referenced in forcing phase 2
271
+ self.ee=ee #A dictionary containing ee info
272
+ self.preAssn=preAssn #A list of lists containing the predefind assignment info
273
+ self.senList=senList
274
+ self.polling=polling
275
+ self.assnLog=[] #To be appended when assignments made, for read out with final product
276
+ self.slLeg=slLeg #Slot Legend. Used for easy refernece of slot times after.
277
+ seqIDs=[int(k[:k.index('_')]) for k in self.slots]
278
+ self.rev=0
279
+ self.noVol=[] #A list to contain keys of slots with no eligible volunteers.
280
+ self.assns=0
281
+ self.maxI=maxI
282
+
283
+ # @debug
284
+ def trackAssn(self,i=0,loc=None):
285
+ self.assns+=1
286
+
287
+ def evalAssnList(self):
288
+ """Enter all predefined assignments into the schedule"""
289
+ #First, iterate through the assignment list. Each record will generate one or more records for the 'slot change log'
290
+ #The slot change log will have one record for each slot for which an assignment (or other specified status change) should be made
291
+ #So assignment log records that indicate a span of multiple slots will generate multiple records in the change log.
292
+ slChLg=[] #Initialize slot change log. It will be a list of lists where each sublist has the necessary info within to pass to the slot assignment function
293
+ #===
294
+ def getKeys(seqId_1,seqId_2,jobNm=None):
295
+ """Returns the keys for all slots that a given assnLog record applies to, job specified or not"""
296
+ myKeys=[]
297
+ if jobNm==None:
298
+ for seqNo in range(seqId_1,seqId_2+1):#Apply to all slots in given range.. +1 due to range fn not being inclusive
299
+ moreKeys=[k for k in self.slots.keys() if k[:len(str(seqNo))+1]==str(seqNo)+'_'] #Pull dict keys for Slots where it is a slot with matching seqNo, regardless of job name
300
+ myKeys.extend(moreKeys)
301
+ else: #job is defined
302
+ for seqNo in range(seqId_1,seqId_2+1):
303
+ myKeys.append(str(seqNo)+'_'+jobNm)
304
+ return myKeys
305
+ #===
306
+ #Here we get down to business - Reading the assignment log, generating records in the slotChangeLog, and then evaluating those changes
307
+ for myAssn in self.preAssn:
308
+ if myAssn[0]==1: #Only evaluate those with '1' in 'Active' (first) column
309
+ assnTp=myAssn[1]
310
+ if (myAssn[5]=="" or myAssn[5]==None): jb=None #grab job name
311
+ else: jb=myAssn[5]
312
+ if (myAssn[4]=="" or myAssn[4]==None): asgne=None #grab assignee
313
+ else: asgne=myAssn[4]
314
+ keys=getKeys(myAssn[2],myAssn[3],jb) #pull all the keys for slots this particular assn list item applies to
315
+ for k in keys:
316
+ slChLg.append([k,self,assnTp,asgne]) #Add record(s) to the slot change long, one for each record.
317
+ #Now that the slChLg is made, carry out the function that reads it record by record and goes and modifies the slots
318
+ def evalLogRec(rec):
319
+ """Carry out the 'assn' method on the associated slot with relevant data from Assn log"""
320
+ if rec[0] in list(self.slots.keys()): #Only assign if the guy actually
321
+ self.slots[rec[0]].assn(rec[1],rec[2],rec[3],fromList=True)
322
+ else: self.assnLog.append('Could not assign ee '+str(rec[3])+' (Assntype='+str(rec[2])+") because slot wasn't created via All_Slots tab")
323
+ for rec in slChLg:
324
+ evalLogRec(rec)
325
+
326
+ def proofEligVol(self):
327
+ """This clears the eligVol lists for all slots of the eeID's where the person isn't eligible"""
328
+ #Necessary because had to make slots before making schedule, so wasn't able to actually test if slOK before assigning ee to slot when initializing everything
329
+ for k in self.slots:
330
+ s=self.slots[k]
331
+ for e in s.eligVol:
332
+ if self.ee[e].slOK(self,s,poll=tls.viewTBL('allPollData',filterOn=[('eeid',e)])[0],pt=False) is not True:
333
+ s.eligVol.pop(s.eligVol.index(e))
334
+
335
+ def nextSlots(self,force=0):
336
+ """Returns the next most constrained unassigned slot object. If 'forcing'=True then returns list of slots with 0 eligible assignes, ordered by seqID"""
337
+ if force==0: #Proceed with selecting most constrained slot with >=1 potential assignees
338
+ kyNonZero=[s for s in self.slots if (len(self.slots[s].eligVol)>0) and self.slots[s].assnType not in ['WWF','F','V','nV','DNS','N'] ] #Get keys for slots with >0 eligVol
339
+ if len(kyNonZero)==0: return None #if none left to assign, just return None
340
+ eligCnts=[len(self.slots[k].eligVol) for k in kyNonZero]
341
+ # print(list(zip([self.slots[s].key() for s in kyNonZero],eligCnts)))
342
+ if eligCnts.count(min(eligCnts))>1: #Case that there are slots tied for most constrained
343
+ slts=[self.slots[s] for s in kyNonZero if len(self.slots[s].eligVol)==min(eligCnts)] #Retrieve the tied slot objects
344
+ totSkills=[sum([len(self.ee[eId].skills) for eId in s.eligVol ]) for s in slts ] #For each slot, make a list of integers, whee each integer is the number of jobs an ee eligible for that slot is trained on. Sum those lists, and the slot with the highest number is selected, since that is correlated with the eligible assignees for that slot having the most ability to cover other slots.
345
+ if totSkills.count(min(totSkills))>1: #Case that 2 slots are tied for minimum of total # of training records for eligible operators
346
+ #Go with the one for which there is an operator with least spots trained... assuming that the operator who is most constrained training wise gets it.. while this is an assumption without great basis, there is at least the point that someone with less training will likely have less refusal hours in the year.. so it may turn to work out ok
347
+ trainRecForLeastTrainedEE=[min([len(self.ee[eId].skills) for eId in s.eligVol ]) for s in slts ] #Same formula as totSkills except min instead of sum
348
+ pickSl=slts[trainRecForLeastTrainedEE.index(min(trainRecForLeastTrainedEE))]
349
+ self.assnLog.append('Slot '+pickSl.key()+' (assnType: '+str(pickSl.assnType)+') chosen as most constrained, was tied for totSkills, chose first')
350
+ return pickSl #Here only 1 return statement because if its a tie we'll just take the first one, which the index function here will give.
351
+ else:
352
+ pickSl=self.slots[ kyNonZero[totSkills.index(max(totSkills))]]
353
+ self.assnLog.append(pickSl.key()+' (assnType: '+str(pickSl.assnType)+') chosen as most constrained, had least training across all volunteers')
354
+ return pickSl #Case of one slot having more totSkills than another. Call slots keys, then index that by the totSKills count tog et the index of the slot we want, and retrieve that from slots,
355
+ else:
356
+ pickSl=self.slots[kyNonZero[eligCnts.index(min(eligCnts))]]
357
+ self.assnLog.append(pickSl.key()+' (assnType: '+str(pickSl.assnType)+') chosen as most constrained, had least ('+str(len(pickSl.eligVol))+') eligible volunteers')
358
+ return pickSl #Retrieve most constrained if not tied with any other.
359
+ elif force==1:#Forcing for the first time. Return list of slots to force into in chronological order
360
+ return sorted([self.slots[s] for s in self.slots if len(self.slots[s].eligVol)==0 and self.slots[s].assnType == None],key=lambda x: x.seqID)
361
+ elif force==2: #Forcing for teh 2nd time. Return all slots. the 'eligibility tracking' isn't perfect so can't filter by it because when people were assigned, would be a pain to make logic to properly remove their 'eligVol' status from the slots which they were no longer eligible for for reasons like max shift length etc. Only removed it for slots happenign at same time
362
+ return sorted([self.slots[s] for s in self.slots if self.slots[s].assnType == 'nV' or self.slots[s].assnType == None],key=lambda x: x.seqID)
363
+
364
+ def pickAssignee(self,sl,tp='V',pt=True,lsOtpt=False):
365
+ """Returns an eeid and the assignment type, either voluntary or forced, or 'N' for None/No staff, for the passed slot"""
366
+ if tp=='V':
367
+ def tblSeq(sl,Acrew,Bcrew):
368
+ """Returns keys for retrieving poll data tables in sequence of priority assignment. With respect to shift, assignment priority goes in CABCA sequence, first FT then Temps"""
369
+ if sl.seqID in [1,2,7,8,13,14,19,20]: homeShift='C'
370
+ elif sl.seqID in [3,4,9,10,15,16,21,22]: homeShift='A'
371
+ else: homeShift='B'
372
+ keys=[]
373
+ seqStr='CABCA'
374
+ cD={'C':'Rock','A':Acrew,'B':Bcrew} #Crew Dict
375
+ for eeTp in ['FT','P','Temp']: #Go through all FT's before temps
376
+ for i in range(3):#code below uses variable homeShift in conjunction with stepping through seqStr to pull out crews in order of priority selection of OT
377
+ keys.append('tbl_'+cD[seqStr[seqStr.index(homeShift)+i]]+eeTp)
378
+ #Keys list is in form: ['tbl_crw1FT','tbl_crw2FT','tbl_crw3FT','tbl_crw1P','tbl_crw2P','tbl_crw3P','tbl_crw1Temp','tbl_crw2Temp','tbl_crw3Temp']
379
+ # Point is, no matter which order they end up (crw1/2/3), we can inject the WWF crew keys in if we are weekend scheduling
380
+ if self.assnWWF==True: #long weekend, wwf to be assigned via refusal and all by program. Insert tbl keys in sequence
381
+ keys.insert(3,'tbl_wFT')
382
+ keys.insert(7,'tbl_wP')
383
+ keys.append('tbl_wT')
384
+ return keys
385
+ #===
386
+ ks=tblSeq(sl,self.Acrew,self.Bcrew)
387
+ ls=[]#For appending eId's in order of their priority selection, if its list output mode
388
+ #Relying on the fact that the tables in the excel sheet were already sequenced in order of refusal hours...
389
+ for k in ks:#Iterate through the tables in provided sequence to pull from crews in sequence of priority pick
390
+ for rec in self.polling[k]: #Iterate through rows in table to pull eeID's in sequence of hours
391
+ if rec[0] is not None: #Error proof on having an empty polling table for a particular crew
392
+ if self.ee[rec[0]].slOK(self,sl,poll=tls.viewTBL('allPollData',filterOn=[('eeid',rec[0])])[0],pt=pt):
393
+ if lsOtpt==False: #Return first person encountered
394
+ return rec[0],'V' #Person has been found
395
+ else: ls.append(rec[0])
396
+ else: pass
397
+ if lsOtpt==True: return ls
398
+ return None,'nV' #No voluntary assignee found
399
+ elif tp=='F':
400
+ if self.pickAssignee(sl,pt=False)[0]!=None: return self.pickAssignee(sl,pt=False) #Check that someone is willing. This branch can be reached in phase 1 forcing when recursing because one force created no takers in previous slot, and now testing to force a slot of different time code. Possible that someone may be willing but shift gap doesnt allow forcing, so need this statement to test on voluntary though command is called from forcing phase
401
+ for i in range(len(self.senList)-1,-1,-1): #Work way down seniority list
402
+ lowManID=int(self.senList[i][2])
403
+ if self.ee[lowManID].slOK(self,sl,tp='F'): return lowManID,'F'
404
+ self.assnLog.append('NO STAFF! No one to force to '+sl.key())
405
+ return None,'N' #No one to force
406
+
407
+ @debug
408
+ def fillOutSched_v3(self,noVol=None,iter=0,pre8={},last=None):
409
+ """Improvement on v2 to prioritize voluntary 8 hour over voluntary 4hr, plus WWF crew assigning on long weekends"""
410
+ if iter>self.maxI: return last
411
+ #Setup
412
+ iter+=1 #increment iteration counter
413
+ WIPschd=deepcopy(self) #WIPschd will have assignments made to it. 'Self' is kept with only the AssnList stuff coming into this point so that across multiple iterations where the NoVol list is potentially expanded, it serves as the blank slate w.r.t slot objects having no assignees
414
+ WIPschd.assnLog=[] #The assnLog up until this function being called is stored with the parent. In each iteration, bits arae added to WIPschd
415
+ #at end of iteration (whther because recursively calling, or finished) this iterations log is appended to the master log
416
+ if iter==1: #First iteration. Initialize noEligVol list. Do not define it this way in future iterations, because you will lose the slots that were discovered that needed to be added!
417
+ WIPschd.noVol=[k for k in WIPschd.slots if len(WIPschd.slots[k].eligVol)==0 and WIPschd.slots[k].assnType not in ['DNS','WWF','V','F']] #pull all slots that are unassigned, with no eligible volunteers either
418
+ else: #2nd and further iterations.. coming here because more slots were found to force and would've passed along in function call... so retrieve them here
419
+ # stragglers=[k for k in last.slots if last.slots[k].assnType in ['nV','N']]
420
+ # noVol.extend(stragglers)
421
+ WIPschd.noVol=noVol
422
+ #===Logging for printout
423
+ WIPschd.assnLog.append('Iteration: '+str(iter)+' || Starting Schedule With '+str(len(WIPschd.noVol))+' Identified Priority Slots, Forcing As Necessary:')
424
+ mynoVol=''
425
+ for x in sorted(WIPschd.noVol,key=lambda k:int(k[:k.index('_')])): mynoVol+=' '+x
426
+ WIPschd.assnLog.append('Prelim Priority Assignment Sequence: '+mynoVol)
427
+ #=====
428
+ #======
429
+ #Phase ZERO... Volunteer assignments or force as necessary for priority slots identified via no volunteers, or became no-vol on previous iterations
430
+ # Slots found to lose all eligVol on prev iterations added to this bunch
431
+ # Also need to check if making a forcing creates the need to make another forcing, and perform recursion in that case as well!
432
+ # Because the forcing list has slots which were determined via prior recurses in the scheduling that identified a dead end requiring forcing, can't use the same method for tracking new slots needing forcings as used for voluntary assignments
433
+ ######
434
+ WIPschd.noVol.extend(list(pre8.keys()))
435
+ for k in sorted(WIPschd.noVol,key=lambda k: int(k[:k.index('_')])): #Iterate through the keys in chronological order
436
+ if k in list(pre8.keys()): #Perform 8 hr assign.. thats why list of pre8 was generated after all
437
+ eId=pre8[k] #pull the eId out of the predefined pr
438
+ k2=[pr[0] for pr in [(x,pre8[x]) for x in pre8.keys()] if pr[1]==eId and abs(int(pr[0][:pr[0].index("_")])-int(k[:k.index("_")]))==1][0]
439
+ #^^^Takes list of all key:val in pre8, pulls out key where seqID within 1 of the seqID for sl k being looked at right now. This is then used to confirm both of these slots are ok to assign. Necessary to shouble check, because other even earlier forcings couldv'e happened after that entry was added to the pre8 dictionary the first time requiring. So can't just blindly assing
440
+ if (True,True)==(WIPschd.ee[eId].slOK(WIPschd,WIPschd.slots[k],pt=False,poll=tls.viewTBL('allPollData',filterOn=[('eeid',eId)])[0]),WIPschd.ee[eId].slOK(WIPschd,WIPschd.slots[k2],pt=False,poll=tls.viewTBL('allPollData',filterOn=[('eeid',eId)])[0])):
441
+ WIPschd.assnLog.append('Making previously identified 8 hour voluntary assignment for '+ WIPschd.ee[eId].lastNm +' to '+k+', '+k2)
442
+ #Checks both slots are ok. Need not check forcing rule because at this stage in process later forcings wouldnt yet exist
443
+ if WIPschd.slots[k].assnType not in ['V','F']: #If k1 was already reached on prev iteration through k loop then don't perform assn function again
444
+ WIPschd.trackAssn(self.assns,loc='pre8 Success 1')
445
+ r=WIPschd.slots[k].assn(WIPschd,assnType="V",slAssignee=eId)
446
+ if WIPschd.slots[k2].assnType not in ['V','F']: #See above
447
+ WIPschd.trackAssn(self.assns,loc='pre8 Success 2')
448
+ r=WIPschd.slots[k2].assn(WIPschd,assnType="V",slAssignee=eId) #Assign both. When the second one gets iterated to, slOK will return False for being assigned on same slot arleady and itll be harmless
449
+ #If this ee was forced earlier and so they are no longer ok for the full8 assign, then the pre8 is effectively skipped and re evaluated in the '8hr assn' phase
450
+ else: #Case of assigning a slot not from pre8 list
451
+ s=WIPschd.slots[k]
452
+ WIPschd.assnLog.append('Forcing if necessary to '+s.dispNm+' '+ WIPschd.slLeg[s.seqID-1][1]+' '+ WIPschd.slLeg[s.seqID-1][2])
453
+ eId,tp=WIPschd.pickAssignee(s,tp='F')
454
+ WIPschd.trackAssn(self.assns,loc='force phase')
455
+ r=WIPschd.slots[k].assn(WIPschd,assnType=tp,slAssignee=eId) #return value to variable r is false if making that force assignments breaks rules around forcing past 48 hrs in week
456
+ if r==False and s.key() not in WIPschd.noVol: #also check if this slot is already in noVol list. Coming to this statement after the fact, I realize there isn't any sensical logic path happening here if the persons forcing broke a rule, but the slot is not in the list. I realize this is because I wrote this if statement at a point where I thought forcings might happen mid-iteration, but after making it so that forcings only happen start of iteration, I think this if statement will never be met. That's because at this point in the algorithm, no voluntary weekend OT has been assigned. but the point of the 'return value' was checking if already-assigned voluntary OT happends before or after the OT that is being forced. But since no voluntary OT has been assigned at all, the return constraint will always be ok. The pickAssignee() method will always return the guy who si to be assigned because if the perosn simply worked enough OT in the week that they can't be forced, then the pickAssignee wouldn't havepicked them anyways
457
+ WIPschd.noVol.append(s.key())
458
+ WIPschd.assnLog.append('The last assignment created a broken schedule where the person' +WIPschd.ee[eId].lastNm+' had a forcing (previously assigned) after 48h in the week (just assigned.) Adding this slot to priority sequence and reiterating')
459
+ self.assnLog.extend(WIPschd.assnLog) #add to master before iterating
460
+ self.assnLog.extend('RETURN A')
461
+ return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd) #WIPschd,'P-Frce Brk' #<-Alt return for debugging
462
+ newK=set([k for k in WIPschd.slots if len(WIPschd.slots[k].eligVol)==0 and WIPschd.slots[k].assnType not in ['WWF','F','V','nV','DNS','N']]) #After assignment, see if anything now needing forcing that hasn't been seen before
463
+ if len(newK-set(WIPschd.noVol))>0: #Case that a forced assignment made someone ineligible for a slot they were marked as the last volunteer in, creating a slot requiring forcing that hasn't been seen before, requiring re iteration
464
+ pullK=newK-set(WIPschd.noVol) #Get the keys for slots that are now without volunteers(could be more than one so use set subtraction)
465
+ WIPschd.assnLog.append('The last assignment resulted in slot(s) '+str(list(pullK))+' having no more eligible volunteers. Those slots are added to the list of slots to force at the start, and a new schedule will be made with updated list of slots to Force')
466
+ WIPschd.noVol.extend(pullK)
467
+ self.assnLog.extend(WIPschd.assnLog) #Add to master before iterating
468
+ self.assnLog.extend('RETURN B')
469
+ return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd) # WIPschd,'P-Bump' #<-Alt return for debugging
470
+ #=====
471
+ #======
472
+ #Phase 0.5 - Make 8 Hour shift assignments previously identified in Phase 1, to set the scene
473
+ #pre8 is a list of tuples (key,eeid) indicating if an ee should be assigned to an 8 hour shift to start. This only happens when they need to be assigned first so as to properly followthrough forcing someone else later.
474
+ #These follow the reverse chronological order of assignment like the rest of voluntary assignments, unlike forcings which were chronological
475
+ #If the slot was put in this list as part of a 'preassigned 8 hours that messed with a forcing previously defined' then it gets evaluated for actually still being feasible with forcingds happening before those 8 that might've come to be after this 'pre8' entry was added. If not valid, no assignment is made and this goes back to being assigned at phase 1 as applicable
476
+ #if not part of the 'pre8' business then proceed like usual with single slot assignment forcing as necessary
477
+ # for k in sorted(list(pre8.keys()),key=lambda k: int(k[:k.index('_')])): #this slot is part of a premade 8hr assignment
478
+ # #Don't need to worry about key sequencing because the process of adding these to dictionary was already doing that and sequencing is maintained
479
+ # eId=pre8[k] #pull the eId out of the predefined pr
480
+ # k2=[pr[0] for pr in [(x,pre8[x]) for x in pre8.keys()] if pr[1]==eId and abs(int(pr[0][:pr[0].index("_")])-int(k[:k.index("_")]))==1][0]
481
+ # #^^^Takes list of all key:val in pre8, pulls out key where seqID within 1 of the seqID for sl k being looked at right now. This is then used to confirm both of these slots are ok to assign. Necessary to shouble check, because other even earlier forcings couldv'e happened after that entry was added to the pre8 dictionary the first time requiring. So can't just blindly assing
482
+ # if (True,True)==(WIPschd.ee[eId].slOK(WIPschd,WIPschd.slots[k],pt=False,poll=tls.viewTBL('allPollData',filterOn=[('eeid',eId)])[0]),WIPschd.ee[eId].slOK(WIPschd,WIPschd.slots[k2],pt=False,poll=tls.viewTBL('allPollData',filterOn=[('eeid',eId)])[0])):
483
+ # #Checks both slots are ok. Need not check forcing rule because at this stage in process later forcings wouldnt yet exist
484
+ # r=WIPschd.slots[k].assn(WIPschd,assnType="V",slAssignee=eId)
485
+ # r=WIPschd.slots[k2].assn(WIPschd,assnType="V",slAssignee=eId) #Assign both. When the second one gets iterated to, slOK will return False for being assigned on same slot arleady and itll be harmless
486
+ #============
487
+ #Phase 1-A... Assigning 8 hour slots as priority, for defined shift times
488
+ #--------
489
+ #Note first ha ddefined this section and used it as a function, but calling the return statement which was meant to eb the end of the fillOutSched function within this was causing recursion problems
490
+ ## First, define function that does the 8 hour assignments. Will use it twice.
491
+ ## def assn8(prK):
492
+ # |-------Sunday--------||----Friday------||------Saturday-----||--------Monday---------|
493
+ prK=[(15,16),(17,18),(13,14),(3,4),(5,6),(1,2),(9,10),(11,12),(7,8),(21,22),(23,24),(19,20)]
494
+ for pr in prK:
495
+ wkColl1=sorted([s for s in WIPschd.slots if (len(WIPschd.slots[s].eligVol)>0) and (WIPschd.slots[s].assnType not in ['WWF','F','V','nV','DNS','N']) and (str(pr[0])==s[:s.index('_')]) ],key=lambda k: len(WIPschd.slots[k].eligVol)) #initialize working Collections, which will contain the unassinged slots in those slot_ID's, in order of least volunteers
496
+ for k1 in wkColl1:
497
+ if WIPschd.slots[k1].assnType not in ['WWF','F','V','nV','DNS','N']: #Last iteration through all eligVols for a k2 didnt find an assignee.... proceed
498
+ wkColl2=sorted([s for s in WIPschd.slots if (len(WIPschd.slots[s].eligVol)>0) and (WIPschd.slots[s].assnType not in ['WWF','F','V','nV','DNS','N']) and (str(pr[1])==s[:s.index('_')]) ],key=lambda k: len(WIPschd.slots[k].eligVol))
499
+ for k2 in wkColl2: #wkColl2 was defined in every iteration of wkColl1 because previous times thorugh the loop could've assigned slots previously in wkColl2 so we want to cut them out each time, so recompute it
500
+ s=0 #Flag to skip out of loop to next k1
501
+ WIPschd.assnLog.append('Attempting to fill 8 hrs '+str(WIPschd.slLeg[int(k1[:k1.index("_")])-1][2])+' '+str(WIPschd.slLeg[int(k1[:k1.index("_")])-1][1])+ ' '+WIPschd.slots[k1].dispNm+' and '+str(WIPschd.slLeg[int(k2[:k2.index("_")])-1][2])+' '+str(WIPschd.slLeg[int(k2[:k2.index("_")])-1][1])+ ' '+WIPschd.slots[k2].dispNm)# s.dispNm+' '+ WIPschd.slLeg[s.seqID-1][1]+' '+ WIPschd.slLeg[s.seqID-1][2])
502
+ #Iterate through every pair of keys to see if someone is ok for the both of them.. if so, assign. Sequenced by most constrained slots
503
+ for eId in WIPschd.pickAssignee(WIPschd.slots[k1],lsOtpt=True): #Retrieve priority sequence list of who gets it
504
+ if eId in WIPschd.slots[k2].eligVol and (WIPschd.ee[eId].wkndHrs+WIPschd.ee[eId].wkdyHrs<=52): #Are they eligVol for the other and ok for 8?
505
+ #Note that at this point, the schedule has only WWF assignments, and forcings that have been identified as required and made. Although 'eligVol' is set for each 4 hr slot in isolation, my thought experiment concludes the 2 eligVol criteria can be used together to check for being ok for a full 8 hour slot in terms of shift gap on each side.. If an 8 hour slot was already assigned in a neighbouring shift, then the eligVol status would've been removed from the far neighbouring slot on this shift, so it wouldn't assign 16 in a row although the eprson would be eligible to use one half of this shift to extend the other to a 12. What isn't caught, however, is taking total week hours beyond 60, since each only cehcks for 4. So that check needs to be added here. Also before an 8 hour assignment is made, need to check if adding those voluntary hours would stop that person from being a valid forcing later in the weekend, that has already been put in the schedule. If so, then assign them and remove the forcing. This latter forcing check I am moving to human review
506
+ if WIPschd.ee[eId].frcOKdblAssn(WIPschd,WIPschd.slots[k1]): #If function returns true.. no forcing rules were broken
507
+ WIPschd.trackAssn(self.assns,loc='asn 8 on shift 1')
508
+ r1=WIPschd.slots[k1].assn(WIPschd,assnType="V",slAssignee=eId)
509
+ WIPschd.trackAssn(self.assns,loc='asn 8 on shift 2')
510
+ r2=WIPschd.slots[k2].assn(WIPschd,assnType="V",slAssignee=eId) #Don't need to check r1 or r2, they are redudnant with passing through frcOKdblAssn to get here
511
+ s=1
512
+ #------ Check if their assignments being made requires that another slot be forced due to losing eligVol
513
+ newK=set([k for k in WIPschd.slots if len(WIPschd.slots[k].eligVol)==0 and WIPschd.slots[k].assnType not in ['WWF','F','V','nV','DNS','N']]) #After assignment, see if anything now needing forcing that hasn't been seen before
514
+ if len(newK-set(WIPschd.noVol))>0: #Case that a forced assignment made someone ineligible for a slot they were marked as the last volunteer in, creating a slot requiring forcing that hasn't been seen before, requiring re iteration
515
+ pullK=newK-set(WIPschd.noVol) #Get the keys for slots that are now without volunteers(could be more than one so use set subtraction)
516
+ WIPschd.assnLog.append('The last assignment resulted in slot(s) '+str(list(pullK))+' having no more eligible volunteers. Those slots are added to the list of slots to force at the start, and a new schedule will be made with updated list of slots to Force')
517
+ WIPschd.noVol.extend(pullK)
518
+ self.assnLog.extend(WIPschd.assnLog) #Add to master before iterating
519
+ self.assnLog.extend('RETURN C')
520
+ return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd) # WIPschd,'P-Bump' #<-Alt return for debugging
521
+ else:
522
+ pre8[k1]=eId
523
+ pre8[k2]=eId
524
+ WIPschd.assnLog.append("Assigning "+str(WIPschd.ee[eId].lastNm)+" a voluntary 8 hour shift to "+str(k1)+", "+str(k2)+" means they can't be forced for a later slot they were forced in for already. Reiterating schedule with this 8 hour assignment on the initial-fill priority list")
525
+ self.assnLog.extend(WIPschd.assnLog)
526
+ self.assnLog.extend('RETURN D')
527
+ return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd)
528
+ if s==1:
529
+ break #Break out of searching through ee's, it got assigned and so, done
530
+ if s==1: break #Break out of incrementing through k2 as k1 has been assignd so we need to look at another k1
531
+ #=============
532
+ #Phase 1-B: Assign straddle shift times.... duplicate code of the section above, with different prK inputs
533
+ prK=[(16,17),(14,15),(18,19),(12,13),(4,5),(2,3),(6,7),(10,11),(8,9),(22,23),(20,21)]
534
+ for pr in prK:
535
+ wkColl1=sorted([s for s in WIPschd.slots if (len(WIPschd.slots[s].eligVol)>0) and (WIPschd.slots[s].assnType not in ['WWF','F','V','nV','DNS','N']) and (str(pr[0])==s[:s.index('_')]) ],key=lambda k: len(WIPschd.slots[k].eligVol)) #initialize working Collections, which will contain the unassinged slots in those slot_ID's, in order of least volunteers
536
+ for k1 in wkColl1:
537
+ if WIPschd.slots[k1].assnType not in ['WWF','F','V','nV','DNS','N']: #Last iteration through all eligVols for a k2 didnt find an assignee.... proceed
538
+ wkColl2=sorted([s for s in WIPschd.slots if (len(WIPschd.slots[s].eligVol)>0) and (WIPschd.slots[s].assnType not in ['WWF','F','V','nV','DNS','N']) and (str(pr[1])==s[:s.index('_')]) ],key=lambda k: len(WIPschd.slots[k].eligVol))
539
+ for k2 in wkColl2: #wkColl2 was defined in every iteration of wkColl1 because previous times thorugh the loop could've assigned slots previously in wkColl2 so we want to cut them out each time, so recompute it
540
+ s=0 #Flag to skip out of loop to next k1
541
+ WIPschd.assnLog.append('Attempting to fill 8 hrs '+str(WIPschd.slLeg[int(k1[:k1.index("_")])-1][2])+' '+str(WIPschd.slLeg[int(k1[:k1.index("_")])-1][1])+ ' '+WIPschd.slots[k1].dispNm+' and '+str(WIPschd.slLeg[int(k2[:k2.index("_")])-1][2])+' '+str(WIPschd.slLeg[int(k2[:k2.index("_")])-1][1])+ ' '+WIPschd.slots[k2].dispNm)# s.dispNm+' '+ WIPschd.slLeg[s.seqID-1][1]+' '+ WIPschd.slLeg[s.seqID-1][2])
542
+ #Iterate through every pair of keys to see if someone is ok for the both of them.. if so, assign. Sequenced by most constrained slots
543
+ for eId in WIPschd.pickAssignee(WIPschd.slots[k1],lsOtpt=True): #Retrieve priority sequence list of who gets it
544
+ if eId in WIPschd.slots[k2].eligVol and (WIPschd.ee[eId].wkndHrs+WIPschd.ee[eId].wkdyHrs<=52): #Are they eligVol for the other and ok for 8?
545
+ #Note that at this point, the schedule has only WWF assignments, and forcings that have been identified as required and made. Although 'eligVol' is set for each 4 hr slot in isolation, my thought experiment concludes the 2 eligVol criteria can be used together to check for being ok for a full 8 hour slot in terms of shift gap on each side.. If an 8 hour slot was already assigned in a neighbouring shift, then the eligVol status would've been removed from the far neighbouring slot on this shift, so it wouldn't assign 16 in a row although the eprson would be eligible to use one half of this shift to extend the other to a 12. What isn't caught, however, is taking total week hours beyond 60, since each only cehcks for 4. So that check needs to be added here. Also before an 8 hour assignment is made, need to check if adding those voluntary hours would stop that person from being a valid forcing later in the weekend, that has already been put in the schedule. If so, then assign them and remove the forcing. This latter forcing check I am moving to human review
546
+ if WIPschd.ee[eId].frcOKdblAssn(WIPschd,WIPschd.slots[k1]): #If function returns true.. no forcing rules were broken
547
+ WIPschd.trackAssn(self.assns,loc='asn 8 straddle 1')
548
+ r1=WIPschd.slots[k1].assn(WIPschd,assnType="V",slAssignee=eId)
549
+ WIPschd.trackAssn(self.assns,loc='asn 8 straddle 2')
550
+ r2=WIPschd.slots[k2].assn(WIPschd,assnType="V",slAssignee=eId) #Don't need to check r1 or r2, they are redudnant with passing through frcOKdblAssn to get here
551
+ s=1
552
+ #------ Check if their assignments being made requires that another slot be forced due to losing eligVol
553
+ newK=set([k for k in WIPschd.slots if len(WIPschd.slots[k].eligVol)==0 and WIPschd.slots[k].assnType not in ['WWF','F','V','nV','DNS','N']]) #After assignment, see if anything now needing forcing that hasn't been seen before
554
+ if len(newK-set(WIPschd.noVol))>0: #Case that a forced assignment made someone ineligible for a slot they were marked as the last volunteer in, creating a slot requiring forcing that hasn't been seen before, requiring re iteration
555
+ pullK=newK-set(WIPschd.noVol) #Get the keys for slots that are now without volunteers(could be more than one so use set subtraction)
556
+ WIPschd.assnLog.append('The last assignment resulted in slot(s) '+str(list(pullK))+' having no more eligible volunteers. Those slots are added to the list of slots to force at the start, and a new schedule will be made with updated list of slots to Force')
557
+ WIPschd.noVol.extend(pullK)
558
+ self.assnLog.extend(WIPschd.assnLog) #Add to master before iterating
559
+ self.assnLog.extend('RETURN G')
560
+ return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd) # WIPschd,'P-Bump' #<-Alt return for debugging
561
+ else:
562
+ pre8[k1]=eId
563
+ pre8[k2]=eId
564
+ WIPschd.assnLog.append("Assigning "+str(WIPschd.ee[eId].lastNm)+" a voluntary 8 hour shift to "+str(k1)+", "+str(k2)+" means they can't be forced for a later slot they were forced in for already. Reiterating schedule with this 8 hour assignment on the initial-fill priority list")
565
+ self.assnLog.extend(WIPschd.assnLog)
566
+ self.assnLog.extend('RETURN H')
567
+ return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd)
568
+ if s==1:
569
+ break #Break out of searching through ee's, it got assigned and so, done
570
+ if s==1: break #Break out of incrementing through k2 as k1 has been assignd so we need to look at another k1
571
+ #=============
572
+ #Phase 1-A: Using the function defined.. priority sequence of slots to look for 8hr assns as below
573
+ # |-------Sunday--------||----Friday------||------Saturday-----||--------Monday---------|
574
+ # prK=[(15,16),(17,18),(13,14),(3,4),(5,6),(1,2),(9,10),(11,12),(7,8),(21,22),(23,24),(19,20)]
575
+ # r=assn8(prK)
576
+ # if r==1: self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8)
577
+ #=============
578
+ #Phase 1-B... Assigning 8 hour slots as priority, this time all those that straddle defined shift times
579
+ # |SunA-B|SunC-A|S.B-MonC|SatB-S.C|FriA-B|F.C-A|F.B-S.C|St.A-B|St.C-A|M.A-B|M.C-A
580
+ # prK=[(16,17),(14,15),(18,19),(12,13),(4,5),(2,3),(6,7),(10,11),(8,9),(22,23),(20,21)]
581
+ # r=assn8(prK)#Because the first of the two shifts straddled is always passed in as the first of the pair, the assignment sequence will always be priority choice to the shift the straddle is starting on, then ending on, then off shift.
582
+ # if r==1: self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8)
583
+ #=============
584
+ #=============
585
+ #Phase 2 - Fill any remaining 4 hour holes.
586
+ #(Copied from schedule function v2 phase 2)
587
+ #OldToAssn=[k for k in WIPschd.slots if (len(WIPschd.slots[k].eligVol)>0) and WIPschd.slots[k].assnType not in ['WWF','F','V','nV','DNS','N'] ]
588
+ OldToAssn=[k for k in WIPschd.slots if WIPschd.slots[k].assnType not in ['WWF','F','V','nV','DNS','N'] ]
589
+ NumSl=len(OldToAssn)
590
+ #===========Printouts for Reporting
591
+ appn='Second Phase Assignments (Sequence by Most Constrained): '+str(NumSl)+' slots || '
592
+ for x in OldToAssn: appn+=x+' - '
593
+ WIPschd.assnLog.append(appn)
594
+ for i in range(NumSl): #Iterate across all slots identified at the start
595
+ # NewToAssn=[k for k in WIPschd.slots if (len(WIPschd.slots[k].eligVol)>0) and WIPschd.slots[k].assnType not in ['WWF','F','V','nV','DNS','N'] ]
596
+ NewToAssn=[k for k in WIPschd.slots if WIPschd.slots[k].assnType not in ['WWF','F','V','DNS','N','nV'] ]
597
+ #===========Printouts for troubleshooting
598
+ # appn='Iter: '+str(i)+' | OldToAssn: '
599
+ # for x in OldToAssn: appn+=x
600
+ # appn+=' | NewToAssn: '
601
+ # for x in NewToAssn: appn+=x
602
+ # WIPschd.assnLog.append(appn)
603
+ #============
604
+ slLost=set(OldToAssn)-set(NewToAssn)
605
+ if len(slLost)<2:
606
+ #Case that this is iteration one (old-new=0) OR this is the expected most common case of the last assignment having been made means that NewToAssn is only missing that last slot, as compared to the Old. If >=2, this means that the last assignment made has resulted in no eligible volunteers for a slot, so there are 2 or more slots missing from NewToAssn list
607
+ OldToAssn=NewToAssn #consider the new as old for next iteration
608
+ curS=WIPschd.nextSlots() #Pick most constrained of all avail slots
609
+ preNoVol=set([k for k in WIPschd.slots if len(WIPschd.slots[k].eligVol)==0 and WIPschd.slots[k].assnType not in ['WWF','F','V','nV','DNS','N']]) #For comparison against after assignment
610
+ if curS is not None:
611
+ lastK=curS.key() #In case needed to remove this slot from set of keys to add to WIPschd.noVol in next iter if multiple slots made to require forcing
612
+ WIPschd.assnLog.append('Looking to voluntarily assign to '+curS.dispNm+' '+ WIPschd.slLeg[curS.seqID-1][1]+' '+ WIPschd.slLeg[curS.seqID-1][2])
613
+ eId,tp=WIPschd.pickAssignee(curS)
614
+ WIPschd.trackAssn(self.assns,loc='4 hr assn')
615
+ r=WIPschd.slots[curS.key()].assn(WIPschd,assnType=tp,slAssignee=eId) #Note the assign method acts on original, not deepcopy, retrieved via key
616
+ newK=set([k for k in WIPschd.slots if len(WIPschd.slots[k].eligVol)==0 and WIPschd.slots[k].assnType not in ['WWF','F','V','nV','DNS','N']]) #After assignment, see if anything now needing forcing that hasn't been seen before
617
+ if len(newK-set(WIPschd.noVol))>0: #Case that a forced assignment made someone ineligible for a slot they were marked as the last volunteer in, creating a slot requiring forcing that hasn't been seen before, requiring re iteration
618
+ pullK=newK-set(WIPschd.noVol) #Get the keys for slots that are now without volunteers(could be more than one so use set subtraction)
619
+ WIPschd.assnLog.append('The last assignment resulted in slot(s) '+str(list(pullK))+' having no more eligible volunteers. Those slots are added to the list of slots to force at the start, and a new schedule will be made with updated list of slots to Force')
620
+ WIPschd.noVol.extend(pullK)
621
+ self.assnLog.extend(WIPschd.assnLog) #Add to master before iterating
622
+ self.assnLog.extend('RETURN I')
623
+ return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd) # WIPschd,'P-Bump' #<-Alt return for debugging
624
+ if r==False and curS.key() not in WIPschd.noVol:
625
+ WIPschd.noVol.append(curS.key())
626
+ WIPschd.assnLog.append('The last assignment created a broken schedule where the person ('+WIPschd.ee[eId].lastNm+') had a forcing (previously assigned) after 48h in the week (just assigned.) Adding this slot to priority sequence and reiterating')
627
+ self.assnLog.extend(WIPschd.assnLog)
628
+ self.assnLog.extend('RETURN E')
629
+ return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd) # WIPschd,'V-F Rule' #<-Alt return for debugging
630
+ # if len(postNoVol-preNoVol)>0: #Recurse, an assignment made another slot have noVol
631
+ # WIPschd.assnLog.append('The last assignment caused slot(s) '+str(list(postNoVol-preNoVol))+' to have no more eligible volunteers. A new iteration will take place with these slots getting priority assignment')
632
+ # WIPschd.noVol.extend(list(postNoVol-preNoVol))
633
+ # self.assnLog.extend(WIPschd.assnLog)
634
+ # self.assnLog.extend('RETURN I')
635
+ # return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd)
636
+ WIPschd.assnLog.extend([str(i)+' of '+str(NumSl-1)])
637
+ if i==NumSl-1:
638
+ WIPschd.assnLog.extend(['MADE IT'])
639
+ self.assnLog.extend(WIPschd.assnLog) #Add whats been logged this final iteration to master log
640
+ WIPschd.assnLog=self.assnLog #Replace WIP with the master since WIP sched object being passed as final outcome
641
+ # return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,winner=WIPschd)
642
+ return WIPschd#,'WIN' #Once all voluntary assignments made, schedule done? May need to force more, or go through force function to designate 'no staff' slots
643
+ else: #Case that assignment being made resulted in more slots being left with no EligVol.. add those slots to noVol list and re start the function
644
+ WIPschd.noVol.extend([k for k in slLost if k!=lastK]) #lastK got assigned, so remove it from slLost to get all slots that are identified as needing to be forced due to person last assigned not being available for other slots...
645
+ WIPschd.assnLog.append('The last assignment resulted in slot ('+str([k for k in slLost if k!=lastK])+') having no more eligible volunteers. Those slots are added to the list of slots to force at the start, and a new schedule will be made with updated list of slots to Force')
646
+ self.assnLog.extend(WIPschd.assnLog)
647
+ self.assnLog.extend('RETURN F')
648
+ return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd)# WIPschd,'V-Bump' #<-Alt return for debugging
649
+
650
+
651
+
652
+ # @debug
653
+ def fillOutSched_v2(self,noVol=None,iter=0):
654
+ """Fills out schedule in a recursive way... see blog... when filling with voluntary folks, if a slot with no takers is encountered and has never been identified as such before, then restart from a fresh sched seed where that slot is forced before any voluntary assignments happen"""
655
+ iter+=1
656
+ WIPschd=deepcopy(self) #WIPschd will have assignments made to it. 'Self' is kept with only the AssnList stuff coming into this point so that across multiple iterations where the NoVol list is potentially expanded, it serves as the blank slate
657
+ if iter==1: #First iteration. Initialize noEligVol list. Do not define it this way in future iterations, because you will lose the slots that were discovered that needed to be added!
658
+ WIPschd.noVol=[k for k in WIPschd.slots if len(WIPschd.slots[k].eligVol)==0 and WIPschd.slots[k].assnType not in ['DNS','WWF','V','F']]
659
+ else: #2nd and further iterations.. coming here because more slots were found to force and would've passed along in function call... so retrieve them here
660
+ WIPschd.noVol=noVol
661
+ #===Logging for printout
662
+ WIPschd.assnLog.append('Iteration: '+str(iter)+' || Starting Schedule With Identified Priority Slots, Forcing As Necessary:')
663
+ mynoVol=''
664
+ for x in sorted(WIPschd.noVol,key=lambda k:int(k[:k.index('_')])): mynoVol+=' '+x
665
+ WIPschd.assnLog.append('Prelim Priority Assignment Sequence: '+mynoVol)
666
+ #=====
667
+ #Initial phase.. Volunteer assignments or force as necessary for priority slots identified via no volunteers, or became no-vol on previous iterations
668
+ # Slots found to lose all eligVol on prev iterations added to this bunch
669
+ # Also need to check if making a forcing creates the need to make another forcing, and perform recursion in that case as well!
670
+ # Because the forcing list has slots which were determined via prior recurses in the scheduling that identified a dead end requiring forcing, can't use the same method for tracking new slots needing forcings as used for voluntary assignments
671
+ for k in sorted(WIPschd.noVol,key=lambda k: int(k[:k.index('_')])): #Iterate through the keys in chronological order
672
+ s=WIPschd.slots[k]
673
+ WIPschd.assnLog.append('Looking to Force to '+s.dispNm+' '+ WIPschd.slLeg[s.seqID-1][1]+' '+ WIPschd.slLeg[s.seqID-1][2])
674
+ eId,tp=WIPschd.pickAssignee(s,tp='F')
675
+ r=s.assn(WIPschd,assnType=tp,slAssignee=eId)
676
+ if r==False and s.key() not in WIPschd.noVol:
677
+ WIPschd.noVol.append(s.key())
678
+ return self.fillOutSched_v2(WIPschd.noVol,iter) #WIPschd,'P-Frce Brk' #<-Alt return for debugging
679
+ newK=set([k for k in WIPschd.slots if len(WIPschd.slots[k].eligVol)==0 and WIPschd.slots[k].assnType not in ['WWF','F','V','nV','DNS','N']]) #After assignment, see if anything now needing forcing that hasn't been seen before
680
+ if len(newK-set(WIPschd.noVol))>0: #Case that a forced assignment made someone ineligible for a slot they were marked as a volunteer in
681
+ pullK=newK-set(WIPschd.noVol) #Get the keys for slots that are now without volunteers(could be more than one so use set subtraction)
682
+ WIPschd.assnLog.append('The last assignment resulted in slot(s) '+str(list(pullK))+' having no more eligible volunteers. Those slots are added to the list of slots to force at the start, and a new schedule will be made with updated list of slots to Force')
683
+ WIPschd.noVol.extend(pullK)
684
+ self.assnLog.extend(WIPschd.assnLog)
685
+ return self.fillOutSched_v2(WIPschd.noVol,iter) # WIPschd,'P-Bump' #<-Alt return for debugging
686
+
687
+ #Second phase - Follow most constrained slot to amke assignments
688
+ # If a slot gets reduced to no eligible volunteers, track it to the priority list and restart
689
+ OldToAssn=[k for k in WIPschd.slots if (len(WIPschd.slots[k].eligVol)>0) and WIPschd.slots[k].assnType not in ['WWF','F','V','nV','DNS','N'] ]
690
+ NumSl=len(OldToAssn)
691
+ #===========Printouts for Reporting
692
+ appn='Second Phase Assignments (Sequence by Most Constrained): '+str(NumSl)+' slots || '
693
+ for x in OldToAssn: appn+=x+' - '
694
+ WIPschd.assnLog.append(appn)
695
+ for i in range(NumSl): #Iterate across all slots identified at the start
696
+ NewToAssn=[k for k in WIPschd.slots if (len(WIPschd.slots[k].eligVol)>0) and WIPschd.slots[k].assnType not in ['WWF','F','V','nV','DNS','N'] ]
697
+ #===========Printouts for troubleshooting
698
+ # appn='Iter: '+str(i)+' | OldToAssn: '
699
+ # for x in OldToAssn: appn+=x
700
+ # appn+=' | NewToAssn: '
701
+ # for x in NewToAssn: appn+=x
702
+ # WIPschd.assnLog.append(appn)
703
+ #============
704
+ slLost=set(OldToAssn)-set(NewToAssn)
705
+ if len(slLost)<2:
706
+ #Case that this is iteration one (old-new=0) OR this is the expected most common case of the last assignment having been made means that NewToAssn is only missing that last slot, as compared to the Old. If >=2, this means that the last assignment made has resulted in no eligible volunteers for a slot, so there are 2 or more slots missing from NewToAssn list
707
+ OldToAssn=NewToAssn #consider the new as old for next iteration
708
+ curS=WIPschd.nextSlots() #Pick most constrained of all avail slots
709
+ lastK=curS.key() #In case needed to remove this slot from set of keys to add to WIPschd.noVol in next iter if multiple slots made to require forcing
710
+ if curS is not None:
711
+ WIPschd.assnLog.append('Looking to voluntarily assign to '+curS.dispNm+' '+ WIPschd.slLeg[curS.seqID-1][1]+' '+ WIPschd.slLeg[curS.seqID-1][2])
712
+ eId,tp=WIPschd.pickAssignee(curS)
713
+ r=WIPschd.slots[curS.key()].assn(WIPschd,assnType=tp,slAssignee=eId) #Note the assign method acts on original, not deepcopy, retrieved via key
714
+ if r==False and curS.key() not in WIPschd.noVol:
715
+ WIPschd.noVol.append(curS.key())
716
+ return self.fillOutSched_v2(WIPschd.noVol,iter) # WIPschd,'V-F Rule' #<-Alt return for debugging
717
+ if i==NumSl-1:return WIPschd#,'WIN' #Once all voluntary assignments made, schedule done? May need to force more, or go through force function to designate 'no staff' slots
718
+ else: #Case that assignment being made resulted in more slots being left with no EligVol.. add those slots to noVol list and re start the function
719
+ WIPschd.assnLog.append('The last assignment resulted in 1 or more other slots having no more eligible volunteers. Those slots are added to the list of slots to force at the start, and a new schedule will be made with updated list of slots to Force')
720
+ WIPschd.noVol.extend([k for k in slLost if k!=lastK]) #lastK got assigned, so remove it from slLost to get all slots that are identified as needing to be forced due to person last assigned not being available for other slots...
721
+ return self.fillOutSched_v2(WIPschd.noVol,iter)# WIPschd,'V-Bump' #<-Alt return for debugging
722
+ #Third phase
723
+ # 3. Final Forced Filling
724
+ # WIPschd.assnLog.append('Final Forcing Phase... Forcing to slots with no more eligible volunteers after having assigned voluntary OT')
725
+ # sls=WIPschd.nextSlots(force=2)
726
+ # for s in sls:
727
+ # WIPschd.assnLog.append('Forcing Phase 2 on '+s.dispNm+' '+ WIPschd.slLeg[s.seqID-1][1]+' '+ WIPschd.slLeg[s.seqID-1][2])
728
+ # eId,tp=WIPschd.pickAssignee(s,tp='F')
729
+ # s.assn(WIPschd,assnType=tp,slAssignee=eId)
730
+
731
+ def fillOutSched(self):
732
+ """Having made the predetermined assignments, fill in the voids in the schedule"""
733
+ #Algorithm is basically:
734
+ # 1. Force staff for slots with no eligible assignees
735
+ # 2. Iterate through unassigned slots in sequence of which is most constrained
736
+ # Assign staff in order of who gets priority pick at the slot
737
+ # 3. Force for slots that had no eligible after giivng the eligble their voluntary choices
738
+ # If no forcing availability, label as such and move on
739
+ #End when no more unassigned slots left
740
+
741
+ #Proceed with carrying out the algorithm:
742
+ # 1. Initial Forcing
743
+ sls=self.nextSlots(force=1)
744
+ self.assnLog.append('Initial Forcing phase... Forcing to slots with no eligible volunteers')
745
+ for s in sls:
746
+ self.assnLog.append('Looking to Force to '+s.dispNm+' '+ self.slLeg[s.seqID-1][1]+' '+ self.slLeg[s.seqID-1][2])
747
+ eId,tp=self.pickAssignee(s,tp='F')
748
+ s.assn(self,assnType=tp,slAssignee=eId)
749
+ # 2. Voluntary Filling
750
+ self.assnLog.append('Voluntary Assignment Phase... Assigning slots in sequence of most to least constrained by number of eligible volunteers')
751
+ sls=deepcopy(self.slots)
752
+ toAssn=[s for s in self.slots if (len(self.slots[s].eligVol)>0) and self.slots[s].assnType not in ['WWF','F','V','nV','DNS','N'] ]
753
+ #toAssn is the same as the first action in nextSlots().. needed here to iterate the right number of times
754
+ for i in range(len(toAssn)): #Iterate across all slots,
755
+ s=self.nextSlots() #Pick most constrained of all avail slots
756
+ if s is not None:
757
+ self.assnLog.append('Looking to voluntarily assign to '+s.dispNm+' '+ self.slLeg[s.seqID-1][1]+' '+ self.slLeg[s.seqID-1][2])
758
+ # sls.pop(s.key()) #Remove that one from set for next iteration
759
+ eId,tp=self.pickAssignee(s)
760
+ self.slots[s.key()].assn(self,assnType=tp,slAssignee=eId) #Note the assign method acts on original, not deepcopy, retrieved via key
761
+ # print(str(i))
762
+ # print(self.slots['8_Labeler'].assnType)
763
+ # 3. Final Forced Filling
764
+ self.assnLog.append('Final Forcing Phase... Forcing to slots with no more eligible volunteers after having assigned voluntary OT')
765
+ sls=self.nextSlots(force=2)
766
+ # print('force slots:')
767
+ # print([x.key() for x in sls])
768
+ for s in sls:
769
+ self.assnLog.append('Forcing Phase 2 on '+s.dispNm+' '+ self.slLeg[s.seqID-1][1]+' '+ self.slLeg[s.seqID-1][2])
770
+ eId,tp=self.pickAssignee(s,tp='F')
771
+ s.assn(self,assnType=tp,slAssignee=eId)
772
+
773
+
774
+ def printToExcel(self):
775
+ """Print all slot assignments to an excel file for human-readable schedule interpretation"""
776
+ #Define Cell styling function
777
+ def styleCell(cl,clType,s=None,horizMergeLength=0):
778
+ for i in range(horizMergeLength+1):
779
+ cl=cl.offset(0,i)
780
+ if clType=='hours':
781
+ cl.font=pyxl.styles.Font(bold=True,size=16,color="00FFFFFF")
782
+ cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='00FF0000',end_color='00FF0000')
783
+ cl.alignment=pyxl.styles.Alignment(horizontal='center')
784
+ cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thick'),
785
+ right=pyxl.styles.Side(border_style='thick'),
786
+ top=pyxl.styles.Side(border_style='thick'),
787
+ bottom=pyxl.styles.Side(border_style='thick'))
788
+ elif clType=='shift':
789
+ cl.font=pyxl.styles.Font(bold=True,size=16)
790
+ cl.alignment=pyxl.styles.Alignment(horizontal='center')
791
+ cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thick'),
792
+ right=pyxl.styles.Side(border_style='thick'),
793
+ top=pyxl.styles.Side(border_style='thick'),
794
+ bottom=pyxl.styles.Side(border_style='thick'))
795
+ elif clType=='day':
796
+ cl.font=pyxl.styles.Font(bold=True,size=28)
797
+ cl.alignment=pyxl.styles.Alignment(horizontal='center')
798
+ cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thick'),
799
+ right=pyxl.styles.Side(border_style='thick'),
800
+ top=pyxl.styles.Side(border_style='thick'),
801
+ bottom=pyxl.styles.Side(border_style='thick'))
802
+ elif clType=='DNS':
803
+ cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='B2B2B2',end_color='B2B2B2')
804
+ cl.font=pyxl.styles.Font(bold=True,size=14)
805
+ cl.alignment=pyxl.styles.Alignment(horizontal='center')
806
+ cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
807
+ right=pyxl.styles.Side(border_style='thin'),
808
+ top=pyxl.styles.Side(border_style='thin'),
809
+ bottom=pyxl.styles.Side(border_style='thin'))
810
+ elif clType=='WWF':
811
+ cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='00B0F0',end_color='00B0F0')
812
+ cl.font=pyxl.styles.Font(bold=True,size=14,color="00FFFFFF")
813
+ cl.alignment=pyxl.styles.Alignment(horizontal='center')
814
+ cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
815
+ right=pyxl.styles.Side(border_style='thin'),
816
+ top=pyxl.styles.Side(border_style='thin'),
817
+ bottom=pyxl.styles.Side(border_style='thin'))
818
+ elif clType=='F':
819
+ cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='CC99FF',end_color='CC99FF')
820
+ cl.font=pyxl.styles.Font(bold=True,size=14)
821
+ cl.alignment=pyxl.styles.Alignment(horizontal='center')
822
+ cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
823
+ right=pyxl.styles.Side(border_style='thin'),
824
+ top=pyxl.styles.Side(border_style='thin'),
825
+ bottom=pyxl.styles.Side(border_style='thin'))
826
+ elif clType=='N':
827
+ cl.font=pyxl.styles.Font(bold=True,size=16,color="00FFFFFF")
828
+ cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='00FF0000',end_color='00FF0000')
829
+ cl.alignment=pyxl.styles.Alignment(horizontal='center')
830
+ cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thick'),
831
+ right=pyxl.styles.Side(border_style='thick'),
832
+ top=pyxl.styles.Side(border_style='thick'),
833
+ bottom=pyxl.styles.Side(border_style='thick'))
834
+ elif clType=='jbNm':
835
+ cl.font=pyxl.styles.Font(bold=True,size=14)
836
+ cl.alignment=pyxl.styles.Alignment(horizontal='left')
837
+ cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
838
+ right=pyxl.styles.Side(border_style='thin'),
839
+ top=pyxl.styles.Side(border_style='thin'),
840
+ bottom=pyxl.styles.Side(border_style='thin'))
841
+ elif clType=='V': #Note that if slot was forced, it will be purple per above if branch, regardless of shift length
842
+ if self.ee[s.assignee].totShiftHrs(s,styling=True)==4: #Colour 4 hour shifts yellow
843
+ cl.font=pyxl.styles.Font(bold=True,size=14)
844
+ cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='00FFC000',end_color='00FFC000')
845
+ cl.alignment=pyxl.styles.Alignment(horizontal='center')
846
+ cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
847
+ right=pyxl.styles.Side(border_style='thin'),
848
+ top=pyxl.styles.Side(border_style='thin'),
849
+ bottom=pyxl.styles.Side(border_style='thin'))
850
+ elif self.ee[s.assignee].totShiftHrs(s,styling=True)==8: #Leave 8 hrs shift white
851
+ cl.font=pyxl.styles.Font(bold=False,size=14)
852
+ cl.alignment=pyxl.styles.Alignment(horizontal='center')
853
+ cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='00FFFFFF',end_color='00FFFFFF')
854
+ cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
855
+ right=pyxl.styles.Side(border_style='thin'),
856
+ top=pyxl.styles.Side(border_style='thin'),
857
+ bottom=pyxl.styles.Side(border_style='thin'))
858
+ elif self.ee[s.assignee].totShiftHrs(s,styling=True)==12: #Colour 12 hrs shift green
859
+ cl.font=pyxl.styles.Font(bold=True,size=14)
860
+ cl.alignment=pyxl.styles.Alignment(horizontal='center')
861
+ cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='0092D050',end_color='0092D050')
862
+ cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
863
+ right=pyxl.styles.Side(border_style='thin'),
864
+ top=pyxl.styles.Side(border_style='thin'),
865
+ bottom=pyxl.styles.Side(border_style='thin'))
866
+
867
+ #Initial Setup
868
+ self.rev+=1
869
+ wb=pyxl.Workbook()
870
+ dest_filename = 'Wknd Sched_Rev '+str(self.rev)+'.xlsx'
871
+ ws = wb.active
872
+ ws.title = "Full Schedule" #Title worksheet for printout
873
+ ws['A1']='Rev '+str(self.rev)
874
+ styleCell(ws['A1'],'shift')
875
+ ws['A2']='Manager'
876
+ styleCell(ws['A2'],'shift')
877
+ ws.column_dimensions['A'].width =22.78
878
+ dys=['Friday','Saturday','Sunday','Monday']
879
+ shifts=['C','A','B']*4
880
+ tSlots=['11p - 3a','3a - 7a','7a - 11a', '11a - 3p','3p-7p', '7p-11p']*4
881
+ for i in range(0,24,6): #Print Days of week
882
+ cl=ws.cell(column=2+i,row=1)
883
+ styleCell(cl,'day',6) #Style all the cells within the merge
884
+ ws.merge_cells(start_row=1, start_column=2+i, end_row=1, end_column=2+i+5)
885
+ cl.value=dys.pop(0)
886
+ for i in range(0,24,2): #Print the shift title
887
+ cl=ws.cell(column=2+i,row=3)
888
+ styleCell(cl,'shift',1)
889
+ ws.merge_cells(start_row=3, start_column=2+i, end_row=3, end_column=2+i+1)
890
+ cl.value=shifts.pop(0)
891
+ for i in range(24): #Print the shift times row
892
+ cl=ws.cell(column=2+i,row=4)
893
+ cl.value=tSlots.pop(0)
894
+ styleCell(cl,'hours')
895
+
896
+ def styleNfill(cl,s):
897
+ if s.assnType=='DNS': cl.value="N/A"
898
+ elif s.assignee is not None:
899
+ val=self.ee[s.assignee].dispNm()
900
+ if s.assnType=='F': val=val+'(F)' #Append force identifier
901
+ cl.value=val
902
+ else:
903
+ s.assnType='N'
904
+ cl.value='NO STAFF'
905
+ styleCell(cl,s.assnType,s)
906
+
907
+ #==================
908
+ #Enter shifts
909
+ #The sequence of keys returned from self.slots.keys() is the same as the slots required were defined in "AllSlots"
910
+ #They should all be in order which would allow for a naive method here but I will generalize it in case someone changes the template
911
+ #And they are not in order after all.
912
+ #The method here is to use a dictionary keyed by job name to track which row a job is printed out to,
913
+ #and to add a job to the dictionary if/when it is encountered for the first time.
914
+ jrD={} #job/Row Dict {job:row}
915
+ r=5 #Start printing job slot info's at row 5 in excel
916
+ for k in self.slots:
917
+ s=self.slots[k] #Retrieve slot
918
+ if s.dispNm not in jrD: #jobRowDict - Add to dict if first time seeing a job
919
+ jrD[s.dispNm]=r
920
+ jbCl=ws.cell(row=r,column=1)
921
+ jbCl.value=s.dispNm
922
+ styleCell(jbCl,'jbNm')
923
+ r+=1 #increment for next one to be observed
924
+ cl=ws.cell(row=jrD[s.dispNm],column=1+s.seqID)
925
+ styleNfill(cl,s)
926
+ #=========================================
927
+ #Go through the schedule to add the (1/3),(1/2),(2/2) etc etc etc slot identifiers for human readibility
928
+ #Define helper function
929
+ def ranges(nums):
930
+ """Returns a list of (open,close) intervals a list spans"""
931
+ nums = sorted(set(nums))
932
+ gaps = [[s, e] for s, e in zip(nums, nums[1:]) if s+1 < e]
933
+ edges = iter(nums[:1] + sum(gaps, []) + nums[-1:])
934
+ return list(zip(edges, edges))
935
+ #===================
936
+ #Proceed with helper function
937
+ for k in self.slots:
938
+ s=self.slots[k] #Retrieve slot
939
+ if s.assignee!=None:
940
+ ids=sorted([self.slots[k].seqID for k in self.ee[s.assignee].assignments])
941
+ rn=ranges(ids)#sets of start-end intervals
942
+ myRn=[x for x in rn if s.seqID>= x[0] and s.seqID<= x[1]][0] #Retrieve start and end sqID for shift containing slot in question
943
+ mysls=[self.slots[k] for k in self.ee[s.assignee].assignments if self.slots[k].seqID>=min(myRn) and self.slots[k].seqID<=max(myRn)] #Retrieve slots
944
+ contents= [[s.dispNm,s.assnType] for s in mysls] #pull slots jobs/assn.Type
945
+ denom=1
946
+ for i in range(len(contents)-1):
947
+ if contents[i]!=contents[i+1]:
948
+ denom+=1
949
+ numrtr=[x.key() for x in sorted(mysls, key=lambda x: x.seqID)].index(s.key())+1 #Check for index of current slot within myRn to see its numerator in sequence
950
+ x=ws.cell(row=jrD[s.dispNm],column=1+s.seqID).value
951
+ if denom!=1:
952
+ ws.cell(row=jrD[s.dispNm],column=1+s.seqID).value=str(x)+' ('+str(numrtr)+'/'+str(denom)+')'
953
+ #======================================================
954
+ #Merge contiguous shifts cells
955
+ #- Define a custom function to facilitate it
956
+ def numInARow(cl,n=1):
957
+ """Given a cell, return the number of cells in a row have the same name in them... Forced shifts break the count. Count starts from 1 for a voluntary shift immediately following a forced shift"""
958
+ nextval=cl.offset(0,1).value
959
+ if nextval is None or nextval=='':
960
+ return n
961
+ elif cl.value!='' and cl.value is not None:
962
+ if '/' in nextval:
963
+ nextval=cl.offset(0,1).value[:cl.offset(0,1).value.index('/')-3]
964
+ curVal=cl.value
965
+ if '/' in cl.value:
966
+ curVal=cl.value[:cl.value.index('/')-3]
967
+ if nextval==curVal:
968
+ return numInARow(cl.offset(0,1),n+1) #Recursive fn. If next cell matches current, use this function on the next one again
969
+ else: return n
970
+ else: return n
971
+ #====== Proceed with above function to be used
972
+ nSkip=0 #Initialize for skipping cells in this loop as applicable
973
+ for rw in range(5,r):
974
+ for i in range(2,26):
975
+ if nSkip<1:
976
+ inArow=numInARow(ws.cell(row=rw,column=i))
977
+ nSkip+=(inArow-1)
978
+ ws.merge_cells(start_row=rw, start_column=i, end_row=rw, end_column=i+inArow-1)
979
+ elif nSkip>0:
980
+ nSkip=nSkip-1 #This facilitates *not* checking cells that have already been merged.. thats because if i,i+1,i+2 merged at i, then when loop increments to i+1, it would give an error when checking on i+2.. need to skip to i+3 after having merged i,+1,+2
981
+ #======================================
982
+ #Format column widths
983
+ #Doesnt appear to be working for some reason :(
984
+ for k in self.slots:
985
+ s=self.slots[k] #Retrieve slot
986
+ ws.column_dimensions[chr(65+s.seqID)].width = max(10.33,len(cl.value),ws.column_dimensions[chr(64+s.seqID)].width-5) #Widen column if new value is wider than any previously existing
987
+ #=============================================
988
+ #Print all forcings
989
+ ws3 = wb.create_sheet(title="FORCINGS")
990
+ ws3.cell(row=1,column=1).value='Employee ID'
991
+ ws3.cell(row=1,column=2).value='Time slot'
992
+ c=0
993
+ for k in [k for k in self.slots if self.slots[k].assnType=='F']:
994
+ ws3.cell(row=2+c,column=1).value=self.ee[self.slots[k].assignee].dispNm()
995
+ ws3.cell(row=2+c,column=2).value=self.slots[k].dispNm+' '+ self.slLeg[self.slots[k].seqID-1][2]+' ('+self.slLeg[self.slots[k].seqID-1][1]+')'
996
+ c+=1
997
+ #=============================================
998
+ #Print assignments to a separate sheet, sequenced by seniority
999
+ ws2 = wb.create_sheet(title="Assignments (Sen'ty)")
1000
+ ws2.cell(row=2,column=1).value='Seniority'
1001
+ ws2.cell(row=2,column=2).value='Employee ID'
1002
+ ws2.cell(row=2,column=3).value='Time slots'
1003
+ # ws2.cell(row=1,column=1).value='Note that the seniority value presented is not actual plant seniority number, but just the sequence of '
1004
+ n=1
1005
+ for i in range(len(self.senList)-1):
1006
+ eId=self.senList[i][2]
1007
+ if len(self.ee[eId].assignments)>0 and self.slots[self.ee[eId].assignments[0]].assnType!='WWF':#If the person has an assignment and isn't WWF, print it
1008
+ n+=1
1009
+ ws2.cell(row=n+1,column=2).value=self.senList[i][0]
1010
+ ws2.cell(row=n+1,column=2).value=eId
1011
+ c=0
1012
+ for k in sorted(self.ee[eId].assignments,key=lambda k:int(k[:k.index('_')])):
1013
+ styleNfill(ws2.cell(row=n+1,column=3+c),self.slots[k])
1014
+ ws2.cell(row=n+1,column=3+c).value=self.slots[k].dispNm+' '+ self.slLeg[self.slots[k].seqID-1][2]+' ('+self.slLeg[self.slots[k].seqID-1][1]+')'
1015
+ c+=1
1016
+ #=============================================
1017
+ #Print assignments to a separate sheet, in alphabetical order by last name
1018
+ ws2 = wb.create_sheet(title="Assignments (Alpha)")
1019
+ ws2.cell(row=2,column=1).value='Last, First'
1020
+ ws2.cell(row=2,column=2).value='Time slots'
1021
+ # ws2.cell(row=1,column=1).value='Note that the seniority value presented is not actual plant seniority number, but just the sequence of '
1022
+ n=1
1023
+ for rec in tls.viewTBL('senRef',sortBy=[('last','ASC')]):
1024
+ eId=rec[2]
1025
+ if len(self.ee[eId].assignments)>0 and self.slots[self.ee[eId].assignments[0]].assnType!='WWF':#If the person has an assignment and isn't WWF, print it
1026
+ n+=1
1027
+ ws2.cell(row=n+1,column=1).value=self.ee[eId].lastNm+', '+self.ee[eId].firstNm[0]+'.'
1028
+ # ws2.cell(row=n+1,column=2).value=eId
1029
+ c=0
1030
+ for k in sorted(self.ee[eId].assignments,key=lambda k:int(k[:k.index('_')])):
1031
+ styleNfill(ws2.cell(row=n+1,column=2+c),self.slots[k])
1032
+ # ws2.cell(row=n+1,column=2+c).value=self.slots[k].dispNm+' '+ self.slLeg[self.slots[k].seqID-1][2]+' ('+self.slLeg[self.slots[k].seqID-1][1]+')'
1033
+ ws2.cell(row=n+1,column=2+c).value=self.slLeg[self.slots[k].seqID-1][2]+' ('+self.slLeg[self.slots[k].seqID-1][1]+')'
1034
+ c+=1
1035
+ #==========================
1036
+
1037
+ wb.save(filename = dest_filename)
1038
+ return dest_filename
1039
+