File size: 99,427 Bytes
2f88c89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
from copy import deepcopy
from ctypes.wintypes import WPARAM
import openpyxl as pyxl
import functools
import SchedBuilderUtyModule as tls
# from importlib import reload

def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)           # 3
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")           # 4
        return value
    return wrapper_debug


class Slot():
    """A single 4 hour time slot for a single job, to be filled by 1 person"""
    def __init__(self,seqID,dispNm,trnNm=None):
        self.trnNm=trnNm #to be used when filtering out staff for training
        self.dispNm=dispNm #to be used for printouts
        self.seqID=seqID
        #self.datetime=   #Determined based on seq. Used in printout of assignments
        self.assignee=None #To store eeid of assignee for printout
        self.assnType=None #e.g. Forced/WWF
        self.slotInShift=0 #1 if first slot in someones shift. 2 if second, etc.
        self.totSlotsInShift=0 # 1 if 4 hours shift, 2 if 8 hour shift, 3 if 12 hour shift
        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
        self.disallowed=[] #EEID's that were specified as not allowed to be assigned to this slot in the Assn List

    def key(self):
        return str(self.seqID)+'_'+self.dispNm
    
    def assn(self,sch,assnType=None,slAssignee=None,fromList=False):
        """Assign a slot to someone, and perform associated variable tracking etc. Returns a bool indicating if a forcing rule was broken or not"""
        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 
            self.disallowed.append(slAssignee)
        else:
            self.assnType=assnType
            self.assignee=slAssignee #eeid
            if assnType in ['WWF','F','V']:
                pass
                # del sch.slots[self.key()] #Remove this slot from the 'openslots' collection if someone was actually assigned
                # #del sch.slots[self.key()]
                # del sch.fslots[self.key()]
            elif assnType=='nV':
                pass
                #del sch.slots[self.key()]
            if slAssignee is not None: #Case of specific assignment, only not follwoed through when its no ee and DNS
                sch.ee[slAssignee].assnBookKeeping(self,sch) #add this slot to the ee's assigned slot dictionary & other tasks
                sch.assignments+=1
                sch.aOnly.append((sch.assignments,sch.ee[slAssignee].dispNm(),self.key(),assnType))
                
        #Logging for printout after
        if assnType!=None and slAssignee!=None:
            logTxt=''
            if fromList==True:
                logTxt+= 'Per Assn List: '
            if assnType=='DNS':
                logTxt+='Removed slot '+self.dispNm+' '+ sch.slLeg[self.seqID-1][2]+' ('+sch.slLeg[self.seqID-1][1]+') from scheduling'
                if slAssignee is not None: logTxt+= ' for ee '+ sch.ee[slAssignee].firstNm[0]+'. '+sch.ee[slAssignee].lastNm
            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]+')'
            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]+')'
            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]+')'
            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]+')'
            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]+')'
            sch.assnLog.append(logTxt)
        if slAssignee!=None and assnType in ['V','F']: #Check if assignment breaks forcing rules
            if sch.ee[slAssignee].frcOK(sch)!=True: #Case that the assignment broke a forcing rule... that slot needs be earlier priority.
                return False
        return True

class ee():
    """A staff persons data as related to weekend scheduling"""
    def __init__(self,snty,crew,id,Last,First,refHrs,wkHrs,wkndHrs=0,skills=[]):
        self.seniority=int(snty)
        if refHrs==None:
            self.refHrs=0
        else:
            self.refHrs=float(refHrs)
        if crew=='wwf':self.wkdyHrs=0
        else: self.wkdyHrs=float(wkHrs)
        self.wkndHrs=float(wkndHrs)
        self.frcHrs=0
        self.lastNm=Last
        self.firstNm=First
        self.eeID=int(id)
        self.crew=crew
        self.assignments=[]#To be appended with slots as they are assigned, keyed as they are in the slot dictionary
        self.skills=skills 
    
    def dispNm(self,slt=None):
        if slt is None:
            if int(self.seniority)>50000: return self.firstNm[0]+'.'+self.lastNm[0]+self.lastNm[1:].lower()+'(T)' #Case of Temps
            else: return (self.firstNm[0]+'.'+self.lastNm[0]+self.lastNm[1:].lower()).replace(' ','-')
        # elif : #Can make functionality to pass in 'MESR' to display name based on some slot criteria?
            # pass
        elif slt=='read':
            return (self.firstNm[0]+'.'+self.lastNm[0]+self.lastNm[1:].lower()).replace(' ','-')

    def frcOK(self,sch):
        """Returns if the present assignments are permissible with rules around forcing limitations"""
        #Used after making assignments to check if need make a slot priority or not
        asn=sorted(self.assignments,key=lambda k:int(k[:k.index('_')])) #Order assignment keys by their slot ID's
        h=self.wkdyHrs #initialize tally
        for k in asn: #For each slot, considering the hours worked upon completion of that workslot
            h+=4
            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
        return True #if the above condition not met.. all good    

    def frcOKdblAssn(self,sch,sl):
        """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"""
        #Used after making assignments to check if need make a slot priority or not
        if self.frcHrs==0: return True
        asn=sorted(self.assignments,key=lambda k:int(k[:k.index('_')])) #Order assignment keys by their slot ID's
        h=self.wkdyHrs #initialize tally
        c=0 #Tracks if the extra 8 have already been applied
        for k in asn: #For each slot, considering the hours worked upon completion of that workslot
            h+=4
            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
                h+=8 #Pretend 2 slots assigned
                c=1 #So this doesn't keep getting reapplied every time moving forward
            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
        return True #if the above condition not met.. all good    

    def assnBookKeeping(self,sl,sch):
        """Carried out when assigned to slot, adjusts tally of eligible volunteers to other slots accordingly"""
        self.assignments.append(sl.key())
        if sl.assnType=='F': self.frcHrs+=4 #Track forced hours
        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
        #   Return keys for slots that the person was counted as an eligible volunteer for
        kys=[k for k in sch.slots if self.eeID in sch.slots[k].eligVol]
        for k in kys:
            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)) 
            #pop the eeId out of eligVol list if the given slot is no longer ok to assign
        #The slOK function does not capture if making a voluntary assignment earlier in the weekend invaldiates a forced assignment later in the weekend.
        #That is captured in a separate function 'frcOK'

    
    def totShiftHrs(self,sl,toFlw=False,styling=False):
        """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"""
        sLen=1 #start off shift length at one because the slot being passed in is always minimum
        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
        if len(assnSeqIDs)==0:
            if toFlw==False:
                return 4 #Case that no slots assigned so far, so if sl assigned then its alone
            else: return 0 #single shift, no following
        else:
            if toFlw==False:
                anch=sl.seqID
                assnSeqIDs.append(anch)
                assnSeqIDs.sort() #sorts integers lowest to highest
                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
                    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)
                        if anch+offset in assnSeqIDs: 
                            sLen+=1
                    return sLen*4 #Return num hours
                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
                    assnSeqIDs=list(set(assnSeqIDs)) #converting to set eliminates duplicates ( in event slot being passed in was already assigned.. it was appended again)
                    def ranges(nums):
                        """Returns a list of (open,close) intervals a list spans"""
                        nums = sorted(set(nums))
                        gaps = [[s, e] for s, e in zip(nums, nums[1:]) if s+1 < e]
                        edges = iter(nums[:1] + sum(gaps, []) + nums[-1:])
                        return list(zip(edges, edges))
                    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
                    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
                    return (myRn[1]-myRn[0]+1)*4 #e.g. range is seqIDs (6,7), thats two slots. 7-6+1=2
            else: #Count # of slots that follow this one consecutively
                i=0 #Count of slots to follow on same shift
                for offset in [1,2]: #Never more than 3 in a row so need only check the 2 following slot seqID's for assignment
                    if anch+offset in assnSeqIDs: 
                        i+=1
                return i
    
    def assnConflict(self,sl):
        """Returns true if someone is already assigned to a slot with same seqID as potential assignment, false if no conflict"""
        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
        if len(assns)==0:return False #if no other assignments.. no conflict
        elif sl.seqID not in assns: return False #if no other assns with same seqID.. no conflict
        elif sl.seqID in assns: return True #if other assignment already amde with same seqID... true, conflict present

    
    def gapOK(self,sl,sch,tp='V'):
        """Returns true if the slot, when assigned, doesn't break the rule for minimum gap between shifts. 12 hours for forcing, 8 for vol"""
        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
        #Just need to check the nearest neighbours aren't with a gap of 1 empty slot
        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
        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 
        if len(deltaNextShifts)==0:
            deltaNextShifts=[0] #Fill with bogus # to give 'ok' result to function
        if len(deltaPrevShifts)==0:
            deltaPrevShifts=[0]
        nextNeighbDist=min(deltaNextShifts)
        prevNeighbDist=min(deltaPrevShifts) 
        #Utility functions:
        def okForLastWkShift():
            if self.crew==sch.Bcrew and sl.seqID-6*(sch.friOT==False)<3+1*(tp=='F'):
                return False #Case of B shift worker being assigned within 8 (or 12 if forced) hrs of last weeks final afternoon shift, no go
            elif self.crew==sch.Acrew and tp=='F' and sl.seqID-6*(sch.friOT==False)<2:
                return False #Case of A shift worker being forced onto first slot of the weekend
            else: return True
        def okForNextWkShift():
            if self.crew=='rock' and tp=='V' and sl.seqID+6*(sch.monOT==False)==23:
                return False #Case of night shift person being assigned 3p-7p afternoon before weekday night shift
            elif self.crew=='rock' and tp=='F' and sl.seqID+6*(sch.monOT==False)>21: 
                return False #Case of night shift forcing.. can't be forced such that <12 hours before next shift
            elif self.crew==sch.Acrew and tp=='F' and sl.seqID+6*(sch.monOT==False)==24:
                return False #Case of day shift ee (next week) being forced 7p-11p the night before
            else: return True
        #=====
        if tp=='V' and (nextNeighbDist==2 or prevNeighbDist==2):
            return False#Distance of one means consecutive slots (shiftlength already checked), distance greater than 2 is gap of 8 hours or more
        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
        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
        else: return False #Some condition not met       

    def slOK(self,sch,sl,poll=0,tp='V',pt=True): #pt is 'print', pVol is 'print did not volunteer' or not
        """Returns True if the slot being tested is ok to be assigned, false if not"""
        #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)
        if sl.assignee==None:#Finding people assigned to WWF slots for some reason.. this fixed that category of problem.
            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
                if (self.eeID not in sl.disallowed): #this ee/slot pairing isn't ruled out in disallowment list
                    if (self.wkndHrs+self.wkdyHrs<60 and sl.seqID<19) or sl.seqID>18: #total week hours ok!
                        if self.assnConflict(sl)==False: #No existing assignment at same time!
                            if self.totShiftHrs(sl)<=12: #This slot wouldn't have a given shift exceed 12 hours
                                if self.gapOK(sl,sch,tp=tp): #This slot being assigned doesn't break a shift gap rule
                                    if self.crew in ['wwf','bud','blue','rock','silver','gold','student']:
                                        if tp=='V':#voluntary: check willigness
                                            if (poll[3+sl.seqID] !="") and (poll[3+sl.seqID] is not None) and (poll[3+sl.seqID]!='n'): #Person is willing!
                                                if self.lastNm=='Bruno': sch.assnLog.append(self.lastNm+' found ok for sl'+sl.key())
                                                return True
                                            else: 
                                                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')
                                                return False
                                        else: 
                                            if (self.wkndHrs+self.wkdyHrs<48) and (self.crew in ['bud','blue','rock','student']): return True #Forced
                                            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')
                                            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')
                                    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')
                                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')
                            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')
                        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')
                    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')
                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')
            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')
        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')        
        return False

class Schedule():
    def __init__(self,Acrew,slots,ee,preAssn,senList,polling,slLeg,sF=False,pNT=True,assnWWF=False,pVol=True,xtraDays=None,maxI=100):
        # self.ftInfoTbl=ftInfoTbl
        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 
        if 'Friday' in xtraDays: self.friOT=True
        else: self.friOT=False
        if 'Monday' in xtraDays: self.monOT=True
        else: self.monOT=False 
        self.pVol=pVol
        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.
        self.pNT=pNT #if True prints '...Not Trained'' statement as applicable when testing if a given slot is ok for someone
        self.sF=sF #suppressFails.. if false, prints out 'failed to schedule' statements
        self.Acrew=Acrew
        if Acrew=='Bud':self.Bcrew='Blue'
        else:self.Bcrew='Bud'
        self.slots= slots  #A collection of Slot objects that compose this schedule
        # self.slots= deepcopy(slots) #Referenced in forcing phase 1
        # self.slots=deepcopy(slots) #Referenced in voluntary assignment phase
        # self.fslots=deepcopy(slots) #Referenced in forcing phase 2
        self.ee=ee #A dictionary containing ee info
        self.preAssn=preAssn #A list of lists containing the predefind assignment info
        self.senList=senList
        self.polling=polling
        self.assnLog=[] #To be appended when assignments made, for read out with final product
        self.slLeg=slLeg #Slot Legend. Used for easy refernece of slot times after.
        seqIDs=[int(k[:k.index('_')]) for k in self.slots]
        self.rev=0
        self.noVol=[] #A list to contain keys of slots with no eligible volunteers.
        self.assignments=1
        self.maxI=maxI
        self.aOnly=[]
        self.assns=0
    
    # @debug
    def trackAssn(self,i=0,loc=None):
        self.assns+=1
    
    def evalAssnList(self):
        """Enter all predefined assignments into the schedule"""
        #First, iterate through the assignment list. Each record will generate one or more records for the 'slot change log'
        #The slot change log will have one record for each slot for which an assignment (or other specified status change) should be made
        #So assignment log records that indicate a span of multiple slots will generate multiple records in the change log.
        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
        #===
        def getKeys(seqId_1,seqId_2,jobNm=None):
            """Returns the keys for all slots that a given assnLog record applies to, job specified or not"""
            myKeys=[]
            if jobNm==None:
                for seqNo in range(seqId_1,seqId_2+1):#Apply to all slots in given range.. +1 due to range fn not being inclusive
                    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
                    myKeys.extend(moreKeys)
            else: #job is defined
                for seqNo in range(seqId_1,seqId_2+1):
                    myKeys.append(str(seqNo)+'_'+jobNm)
            return myKeys
        #===
        #Here we get down to business - Reading the assignment log, generating records in the slotChangeLog, and then evaluating those changes
        for myAssn in self.preAssn:
            if myAssn[0]==1: #Only evaluate those with '1' in 'Active' (first) column
                assnTp=myAssn[1]
                if (myAssn[5]=="" or myAssn[5]==None): jb=None #grab job name
                else: jb=myAssn[5]
                if (myAssn[4]=="" or myAssn[4]==None): asgne=None #grab assignee
                else: asgne=myAssn[4]
                keys=getKeys(myAssn[2],myAssn[3],jb) #pull all the keys for slots this particular assn list item applies to
                for k in keys:
                    slChLg.append([k,self,assnTp,asgne]) #Add record(s) to the slot change long, one for each record.
        #Now that the slChLg is made, carry out the function that reads it record by record and goes and modifies the slots
        def evalLogRec(rec):
            """Carry out the 'assn' method on the associated slot with relevant data from Assn log"""
            if rec[0] in list(self.slots.keys()): #Only assign if the guy actually 
                self.slots[rec[0]].assn(rec[1],rec[2],rec[3],fromList=True)
            else: self.assnLog.append('Could not assign ee '+str(rec[3])+' (Assntype='+str(rec[2])+") because slot wasn't created via All_Slots tab")
        for rec in slChLg:
            evalLogRec(rec)
    
    def proofEligVol(self):
        """This clears the eligVol lists for all slots of the eeID's where the person isn't eligible"""
        #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
        for k in self.slots:
            s=self.slots[k]
            for e in s.eligVol:
                if self.ee[e].slOK(self,s,poll=tls.viewTBL('allPollData',filterOn=[('eeid',e)])[0],pt=False) is not True:
                    s.eligVol.pop(s.eligVol.index(e))

    def nextSlots(self,force=0):
        """Returns the next most constrained unassigned slot object. If 'forcing'=True then returns list of slots with 0 eligible assignes, ordered by seqID"""
        if force==0: #Proceed with selecting most constrained slot with >=1 potential assignees
            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
            if len(kyNonZero)==0: return None #if none left to assign, just return None
            eligCnts=[len(self.slots[k].eligVol) for k in kyNonZero]
            # print(list(zip([self.slots[s].key() for s in kyNonZero],eligCnts)))
            if eligCnts.count(min(eligCnts))>1: #Case that there are slots tied for most constrained
                slts=[self.slots[s] for s in kyNonZero if len(self.slots[s].eligVol)==min(eligCnts)] #Retrieve the tied slot objects
                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.
                if totSkills.count(min(totSkills))>1: #Case that 2 slots are tied for minimum of total # of training records for eligible operators
                    #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
                    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
                    pickSl=slts[trainRecForLeastTrainedEE.index(min(trainRecForLeastTrainedEE))]
                    self.assnLog.append('Slot '+pickSl.key()+' (assnType: '+str(pickSl.assnType)+') chosen as most constrained, was tied for totSkills, chose first')
                    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.
                else: 
                    pickSl=self.slots[ kyNonZero[totSkills.index(max(totSkills))]]
                    self.assnLog.append(pickSl.key()+' (assnType: '+str(pickSl.assnType)+') chosen as most constrained, had least training across all volunteers')
                    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, 
            else:
                pickSl=self.slots[kyNonZero[eligCnts.index(min(eligCnts))]]
                self.assnLog.append(pickSl.key()+' (assnType: '+str(pickSl.assnType)+') chosen as most constrained, had least ('+str(len(pickSl.eligVol))+') eligible volunteers')
                return pickSl #Retrieve most constrained if not tied with any other.
        elif force==1:#Forcing for the first time. Return list of slots to force into in chronological order
            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)
        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
            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)

    def pickAssignee(self,sl,tp='V',pt=True,lsOtpt=False):
        """Returns an eeid and the assignment type, either voluntary or forced, or 'N' for None/No staff, for the passed slot"""
        if tp=='V':
            def tblSeq(sl,Acrew,Bcrew):
                """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"""
                if sl.seqID in [1,2,7,8,13,14,19,20]: homeShift='C'
                elif sl.seqID in [3,4,9,10,15,16,21,22]: homeShift='A'
                else: homeShift='B'
                keys=[]
                seqStr='CABCA'
                cD={'C':'Rock','A':Acrew,'B':Bcrew} #Crew Dict
                for eeTp in ['FT','P','Temp']: #Go through all FT's before temps
                    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
                        keys.append('tbl_'+cD[seqStr[seqStr.index(homeShift)+i]]+eeTp)
                #Keys list is in form: ['tbl_crw1FT','tbl_crw2FT','tbl_crw3FT','tbl_crw1P','tbl_crw2P','tbl_crw3P','tbl_crw1Temp','tbl_crw2Temp','tbl_crw3Temp']
                #  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
                    if self.assnWWF==True: #long weekend, wwf to be assigned via refusal and all by program. Insert tbl keys in sequence
                        keys.insert(3,'tbl_wFT')
                        keys.insert(7,'tbl_wP')
                        keys.append('tbl_wT')
                return keys
            #===
            ks=tblSeq(sl,self.Acrew,self.Bcrew)
            ls=[]#For appending eId's in order of their priority selection, if its list output mode
            #Relying on the fact that the tables in the excel sheet were already sequenced in order of refusal hours...
            for k in ks:#Iterate through the tables in provided sequence to pull from crews in sequence of priority pick
                for rec in self.polling[k]: #Iterate through rows in table to pull eeID's in sequence of hours
                    if rec[0] is not None: #Error proof on having an empty polling table for a particular crew
                        if self.ee[rec[0]].slOK(self,sl,poll=tls.viewTBL('allPollData',filterOn=[('eeid',rec[0])])[0],pt=pt):
                            if lsOtpt==False: #Return first person encountered
                                return rec[0],'V' #Person has been found 
                            else: ls.append(rec[0])
                        else: pass
            if lsOtpt==True: return ls
            return None,'nV' #No voluntary assignee found                                
        elif tp=='F':
            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
            for i in range(len(self.senList)-1,-1,-1): #Work way down seniority list
                lowManID=int(self.senList[i][2])
                if self.ee[lowManID].slOK(self,sl,tp='F'): return lowManID,'F'
            self.assnLog.append('NO STAFF! No one to force to '+sl.key())
            return None,'N' #No one to force
    # @debug
    def checkForceStop(self,tup,iter,assn):
        if tup!=None:
            if iter==tup[0] and assn==tup[1]: return True
            else: return False
        else: return None

    def handleAssnLog(self,WIPschd):
        self.assnLog.extend(WIPschd.assnLog) #Add whats been logged this final iteration to master log
        self.assnLog.extend(['final iteration: '+str(iter)])
        WIPschd.assnLog=self.assnLog #Replace WIP with the master now that master had WIP tacked on to WWF assn since WIP sched object being passed as final outcome
        return WIPschd
        
    
    @debug
    def fillOutSched_v3(self,noVol=None,iter=0,pre8={},last=None,stop=None):
        """Improvement on v2 to prioritize voluntary 8 hour over voluntary 4hr, plus WWF crew assigning on long weekends"""
        if iter>self.maxI: return last
        #Setup
        iter+=1 #increment iteration counter
        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
        WIPschd.assnLog=[] #The assnLog up until this function being called is stored with the parent. In each iteration, bits arae added to WIPschd
        #at end of iteration (whther because recursively calling, or finished) this iterations log is appended to the master log
        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!
            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
        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
            # stragglers=[k for k in last.slots if last.slots[k].assnType in ['nV','N']]
            # noVol.extend(stragglers)
            WIPschd.noVol=noVol
        #===Logging for printout
        WIPschd.assnLog.append('Iteration: '+str(iter)+' ||  Starting Schedule With '+str(len(WIPschd.noVol))+' Identified Priority Slots, Forcing As Necessary:')
        mynoVol=''
        for x in sorted(WIPschd.noVol,key=lambda k:int(k[:k.index('_')])): mynoVol+=' '+x
        WIPschd.assnLog.append('Prelim Priority Assignment Sequence: '+mynoVol)
        #=====
        #======
        #Phase ZERO... Volunteer assignments or force as necessary for priority slots identified via no volunteers, or became no-vol on previous iterations
        #       Slots found to lose all eligVol on prev iterations added to this bunch
        #       Also need to check if making a forcing creates the need to make another forcing, and perform recursion in that case as well!
        #       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
        ######
        WIPschd.noVol.extend(list(pre8.keys()))
        for k in sorted(WIPschd.noVol,key=lambda k: int(k[:k.index('_')])): #Iterate through the keys in chronological order          
            if k in list(pre8.keys()): #Perform 8 hr assign.. thats why list of pre8 was generated after all
                eId=pre8[k] #pull the eId out of the predefined pr
                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] 
                #^^^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 
                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])):
                    WIPschd.assnLog.append('Making previously identified 8 hour voluntary assignment for '+ WIPschd.ee[eId].lastNm +' to '+k+', '+k2)
                    #Checks both slots are ok. Need not check forcing rule because at this stage in process later forcings wouldnt yet exist
                    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
                        WIPschd.trackAssn(self.assns,loc='pre8 Success 1')
                        r=WIPschd.slots[k].assn(WIPschd,assnType="V",slAssignee=eId) 
                        if self.checkForceStop(stop,iter,WIPschd.assignments)==True: 
                            WIPschd=self.handleAssnLog(WIPschd)
                            return WIPschd
                    if WIPschd.slots[k2].assnType not in ['V','F']: #See above
                        WIPschd.trackAssn(self.assns,loc='pre8 Success 2')
                        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
                        if self.checkForceStop(stop,iter,WIPschd.assignments)==True: 
                            WIPschd=self.handleAssnLog(WIPschd)
                            return WIPschd
                #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
            else: #Case of assigning a slot not from pre8 list
                s=WIPschd.slots[k]
                WIPschd.assnLog.append('Forcing if necessary to '+s.dispNm+' '+ WIPschd.slLeg[s.seqID-1][1]+' '+ WIPschd.slLeg[s.seqID-1][2])
                eId,tp=WIPschd.pickAssignee(s,tp='F')
                WIPschd.trackAssn(self.assns,loc='force phase')
                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
                if self.checkForceStop(stop,iter,WIPschd.assignments)==True: 
                    WIPschd=self.handleAssnLog(WIPschd)
                    return WIPschd
                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
                    WIPschd.noVol.append(s.key())
                    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')
                    self.assnLog.extend(WIPschd.assnLog) #add to master before iterating
                    self.assnLog.extend('RETURN A')
                    return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd,stop=stop) #WIPschd,'P-Frce Brk' #<-Alt return for debugging
                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
                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
                    pullK=newK-set(WIPschd.noVol) #Get the keys for slots that are now without volunteers(could be more than one so use set subtraction)
                    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')
                    WIPschd.noVol.extend(pullK)
                    self.assnLog.extend(WIPschd.assnLog) #Add to master before iterating
                    self.assnLog.extend('RETURN B')
                    return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd,stop=stop) # WIPschd,'P-Bump'  #<-Alt return for debugging
        #=====
        #======
        #Phase 0.5 - Make 8 Hour shift assignments previously identified in Phase 1, to set the scene
        #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.
        #These follow the reverse chronological order of assignment like the rest of voluntary assignments, unlike forcings which were chronological
        #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
        #if not part of the 'pre8' business then proceed like usual with single slot assignment forcing as necessary
        # for k in sorted(list(pre8.keys()),key=lambda k: int(k[:k.index('_')])): #this slot is part of a premade 8hr assignment
        #     #Don't need to worry about key sequencing because the process of adding these to dictionary was already doing that and sequencing is maintained
        #     eId=pre8[k] #pull the eId out of the predefined pr
        #     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] 
        #     #^^^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 
        #     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])):
        #         #Checks both slots are ok. Need not check forcing rule because at this stage in process later forcings wouldnt yet exist
        #         r=WIPschd.slots[k].assn(WIPschd,assnType="V",slAssignee=eId) 
        #         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
        #============
        #Phase 1-A... Assigning 8 hour slots as priority, for defined shift times
        #--------
        #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
        ## First, define function that does the 8 hour assignments. Will use it twice.
        ## def assn8(prK):
        #    |-------Sunday--------||----Friday------||------Saturday-----||--------Monday---------|
        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)]
        for pr in prK:
            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
            for k1 in wkColl1:
                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
                    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))
                    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
                        s=0 #Flag to skip out of loop to next k1
                        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])
                        #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
                        for eId in WIPschd.pickAssignee(WIPschd.slots[k1],lsOtpt=True): #Retrieve priority sequence list of who gets it
                            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?
                                #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
                                if WIPschd.ee[eId].frcOKdblAssn(WIPschd,WIPschd.slots[k1]): #If function returns true.. no forcing rules were broken
                                    WIPschd.trackAssn(self.assns,loc='asn 8 on shift 1')
                                    r1=WIPschd.slots[k1].assn(WIPschd,assnType="V",slAssignee=eId)
                                    WIPschd.trackAssn(self.assns,loc='asn 8 on shift 2')
                                    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
                                    if self.checkForceStop(stop,iter,WIPschd.assignments)==True: 
                                        WIPschd=self.handleAssnLog(WIPschd)
                                        return WIPschd
                                    s=1
                                    #------ Check if their assignments being made requires that another slot be forced due to losing eligVol
                                    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
                                    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
                                        pullK=newK-set(WIPschd.noVol) #Get the keys for slots that are now without volunteers(could be more than one so use set subtraction)
                                        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')
                                        WIPschd.noVol.extend(pullK)
                                        self.assnLog.extend(WIPschd.assnLog) #Add to master before iterating
                                        self.assnLog.extend('RETURN C')
                                        return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd,stop=stop) # WIPschd,'P-Bump'  #<-Alt return for debugging
                                else:                                         
                                    pre8[k1]=eId
                                    pre8[k2]=eId
                                    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")
                                    self.assnLog.extend(WIPschd.assnLog)
                                    self.assnLog.extend('RETURN D')
                                    return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd,stop=stop)
                                if s==1: 
                                    break #Break out of searching through ee's, it got assigned and so, done
                        if s==1: break #Break out of incrementing through k2 as k1 has been assignd so we need to look at another k1
        #=============
        #Phase 1-B: Assign straddle shift times.... duplicate code of the section above, with different prK inputs
        prK=[(16,17),(14,15),(18,19),(12,13),(4,5),(2,3),(6,7),(10,11),(8,9),(22,23),(20,21)]
        for pr in prK:
            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
            for k1 in wkColl1:
                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
                    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))
                    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
                        s=0 #Flag to skip out of loop to next k1
                        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])
                        #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
                        for eId in WIPschd.pickAssignee(WIPschd.slots[k1],lsOtpt=True): #Retrieve priority sequence list of who gets it
                            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?
                                #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
                                if WIPschd.ee[eId].frcOKdblAssn(WIPschd,WIPschd.slots[k1]): #If function returns true.. no forcing rules were broken
                                    WIPschd.trackAssn(self.assns,loc='asn 8 straddle 1')
                                    r1=WIPschd.slots[k1].assn(WIPschd,assnType="V",slAssignee=eId)
                                    if self.checkForceStop(stop,iter,WIPschd.assignments)==True: 
                                        WIPschd=self.handleAssnLog(WIPschd)
                                        return WIPschd
                                    WIPschd.trackAssn(self.assns,loc='asn 8 straddle 2')
                                    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
                                    if self.checkForceStop(stop,iter,WIPschd.assignments)==True: 
                                        WIPschd=self.handleAssnLog(WIPschd)
                                        return WIPschd
                                    s=1
                                    #------ Check if their assignments being made requires that another slot be forced due to losing eligVol
                                    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
                                    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
                                        pullK=newK-set(WIPschd.noVol) #Get the keys for slots that are now without volunteers(could be more than one so use set subtraction)
                                        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')
                                        WIPschd.noVol.extend(pullK)
                                        self.assnLog.extend(WIPschd.assnLog) #Add to master before iterating
                                        self.assnLog.extend('RETURN G')
                                        return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd,stop=stop) # WIPschd,'P-Bump'  #<-Alt return for debugging
                                else:                                         
                                    pre8[k1]=eId
                                    pre8[k2]=eId
                                    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")
                                    self.assnLog.extend(WIPschd.assnLog)
                                    self.assnLog.extend('RETURN H')
                                    return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd,stop=stop)
                                if s==1: 
                                    break #Break out of searching through ee's, it got assigned and so, done
                        if s==1: break #Break out of incrementing through k2 as k1 has been assignd so we need to look at another k1
        #=============
        #Phase 1-A: Using the function defined.. priority sequence of slots to look for 8hr assns as below
        #    |-------Sunday--------||----Friday------||------Saturday-----||--------Monday---------|
        # 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)]
        # r=assn8(prK) 
        # if r==1: self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8)
        #=============
        #Phase 1-B... Assigning 8 hour slots as priority, this time all those that straddle defined shift times
        #   |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
        # prK=[(16,17),(14,15),(18,19),(12,13),(4,5),(2,3),(6,7),(10,11),(8,9),(22,23),(20,21)]
        # 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.
        # if r==1: self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8)
        #=============
        #=============
        #Phase 2 - Fill any remaining 4 hour holes.
        #(Copied from schedule function v2 phase 2)
        #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'] ]
        OldToAssn=[k for k in WIPschd.slots if WIPschd.slots[k].assnType not in ['WWF','F','V','nV','DNS','N'] ]
        NumSl=len(OldToAssn)
        #===========Printouts for Reporting
        appn='Second Phase Assignments (Sequence by Most Constrained): '+str(NumSl)+' slots  ||  '
        for x in OldToAssn: appn+=x+' - '
        WIPschd.assnLog.append(appn)
        for i in range(NumSl): #Iterate across all slots identified at the start
            # 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'] ]
            NewToAssn=[k for k in WIPschd.slots if WIPschd.slots[k].assnType not in ['WWF','F','V','DNS','N','nV'] ]
            #===========Printouts for troubleshooting
            # appn='Iter: '+str(i)+' | OldToAssn: '
            # for x in OldToAssn: appn+=x
            # appn+='  | NewToAssn: '
            # for x in NewToAssn: appn+=x
            # WIPschd.assnLog.append(appn)
            #============
            slLost=set(OldToAssn)-set(NewToAssn)
            if len(slLost)<2:
                #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
                OldToAssn=NewToAssn #consider the new as old for next iteration
                curS=WIPschd.nextSlots() #Pick most constrained of all avail slots
                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
                if curS is not None:
                    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
                    WIPschd.assnLog.append('Looking to voluntarily assign to '+curS.dispNm+' '+ WIPschd.slLeg[curS.seqID-1][1]+' '+ WIPschd.slLeg[curS.seqID-1][2])
                    eId,tp=WIPschd.pickAssignee(curS)
                    WIPschd.trackAssn(self.assns,loc='4 hr assn')
                    r=WIPschd.slots[curS.key()].assn(WIPschd,assnType=tp,slAssignee=eId) #Note the assign method acts on original, not deepcopy, retrieved via key
                    if self.checkForceStop(stop,iter,WIPschd.assignments)==True: 
                            WIPschd=self.handleAssnLog(WIPschd)
                            return WIPschd
                    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
                    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
                        pullK=newK-set(WIPschd.noVol) #Get the keys for slots that are now without volunteers(could be more than one so use set subtraction)
                        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')
                        WIPschd.noVol.extend(pullK)
                        self.assnLog.extend(WIPschd.assnLog) #Add to master before iterating
                        self.assnLog.extend('RETURN I')
                        return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd,stop=stop) # WIPschd,'P-Bump'  #<-Alt return for debugging
                    if r==False and curS.key() not in WIPschd.noVol:
                        WIPschd.noVol.append(curS.key())
                        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')
                        self.assnLog.extend(WIPschd.assnLog)
                        self.assnLog.extend('RETURN E')
                        return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd,stop=stop) # WIPschd,'V-F Rule'   #<-Alt return for debugging
                    # if len(postNoVol-preNoVol)>0: #Recurse, an assignment made another slot have noVol
                    #     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')
                    #     WIPschd.noVol.extend(list(postNoVol-preNoVol))
                    #     self.assnLog.extend(WIPschd.assnLog)
                    #     self.assnLog.extend('RETURN I')
                    #     return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd)
                WIPschd.assnLog.extend([str(i)+' of '+str(NumSl-1)])
                if i==NumSl-1:
                    WIPschd.assnLog.extend(['Done.'])
                    self.assnLog.extend(WIPschd.assnLog) #Add whats been logged this final iteration to master log
                    WIPschd.assnLog=self.assnLog #Replace WIP with the master now that master had WIP tacked on to WWF assn since WIP sched object being passed as final outcome
                    self.assnLog.extend(['final iteration: '+str(iter)])
                    # return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,winner=WIPschd)
                    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
            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
                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...
                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')
                self.assnLog.extend(WIPschd.assnLog)
                self.assnLog.extend('RETURN F')
                return self.fillOutSched_v3(WIPschd.noVol,iter,pre8=pre8,last=WIPschd,stop=stop)# WIPschd,'V-Bump' #<-Alt return for debugging 

                                

    # @debug
    def fillOutSched_v2(self,noVol=None,iter=0):
        """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"""
        iter+=1
        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 
        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!
            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']]
        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
            WIPschd.noVol=noVol
        #===Logging for printout
        WIPschd.assnLog.append('Iteration: '+str(iter)+' ||  Starting Schedule With Identified Priority Slots, Forcing As Necessary:')
        mynoVol=''
        for x in sorted(WIPschd.noVol,key=lambda k:int(k[:k.index('_')])): mynoVol+=' '+x
        WIPschd.assnLog.append('Prelim Priority Assignment Sequence: '+mynoVol)
        #=====
        #Initial phase.. Volunteer assignments or force as necessary for priority slots identified via no volunteers, or became no-vol on previous iterations
        #       Slots found to lose all eligVol on prev iterations added to this bunch
        #       Also need to check if making a forcing creates the need to make another forcing, and perform recursion in that case as well!
        #       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
        for k in sorted(WIPschd.noVol,key=lambda k: int(k[:k.index('_')])): #Iterate through the keys in chronological order          
            s=WIPschd.slots[k]
            WIPschd.assnLog.append('Looking to Force to '+s.dispNm+' '+ WIPschd.slLeg[s.seqID-1][1]+' '+ WIPschd.slLeg[s.seqID-1][2])
            eId,tp=WIPschd.pickAssignee(s,tp='F')
            r=s.assn(WIPschd,assnType=tp,slAssignee=eId)
            if r==False and s.key() not in WIPschd.noVol:
                WIPschd.noVol.append(s.key())
                return self.fillOutSched_v2(WIPschd.noVol,iter) #WIPschd,'P-Frce Brk' #<-Alt return for debugging
            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
            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
                pullK=newK-set(WIPschd.noVol) #Get the keys for slots that are now without volunteers(could be more than one so use set subtraction)
                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')
                WIPschd.noVol.extend(pullK)
                self.assnLog.extend(WIPschd.assnLog)
                return self.fillOutSched_v2(WIPschd.noVol,iter) # WIPschd,'P-Bump'  #<-Alt return for debugging

        #Second phase - Follow most constrained slot to amke assignments
        #      If a slot gets reduced to no eligible volunteers, track it to the priority list and restart
        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'] ]
        NumSl=len(OldToAssn)
        #===========Printouts for Reporting
        appn='Second Phase Assignments (Sequence by Most Constrained): '+str(NumSl)+' slots  ||  '
        for x in OldToAssn: appn+=x+' - '
        WIPschd.assnLog.append(appn)
        for i in range(NumSl): #Iterate across all slots identified at the start
            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'] ]
            #===========Printouts for troubleshooting
            # appn='Iter: '+str(i)+' | OldToAssn: '
            # for x in OldToAssn: appn+=x
            # appn+='  | NewToAssn: '
            # for x in NewToAssn: appn+=x
            # WIPschd.assnLog.append(appn)
            #============
            slLost=set(OldToAssn)-set(NewToAssn)
            if len(slLost)<2:
                #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
                OldToAssn=NewToAssn #consider the new as old for next iteration
                curS=WIPschd.nextSlots() #Pick most constrained of all avail slots
                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
                if curS is not None:
                    WIPschd.assnLog.append('Looking to voluntarily assign to '+curS.dispNm+' '+ WIPschd.slLeg[curS.seqID-1][1]+' '+ WIPschd.slLeg[curS.seqID-1][2])
                    eId,tp=WIPschd.pickAssignee(curS)
                    r=WIPschd.slots[curS.key()].assn(WIPschd,assnType=tp,slAssignee=eId) #Note the assign method acts on original, not deepcopy, retrieved via key
                    if r==False and curS.key() not in WIPschd.noVol:
                        WIPschd.noVol.append(curS.key())
                        return self.fillOutSched_v2(WIPschd.noVol,iter) # WIPschd,'V-F Rule'   #<-Alt return for debugging
                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
            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
                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')
                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...
                return self.fillOutSched_v2(WIPschd.noVol,iter)# WIPschd,'V-Bump' #<-Alt return for debugging
        #Third phase
        # 3. Final Forced Filling
        # WIPschd.assnLog.append('Final Forcing Phase... Forcing to slots with no more eligible volunteers after having assigned voluntary OT')
        # sls=WIPschd.nextSlots(force=2)
        # for s in sls:
        #     WIPschd.assnLog.append('Forcing Phase 2 on '+s.dispNm+' '+ WIPschd.slLeg[s.seqID-1][1]+' '+ WIPschd.slLeg[s.seqID-1][2])
        #     eId,tp=WIPschd.pickAssignee(s,tp='F')
        #     s.assn(WIPschd,assnType=tp,slAssignee=eId)

    def fillOutSched(self):
        """Having made the predetermined assignments, fill in the voids in the schedule"""
        #Algorithm is basically:
        # 1. Force staff for slots with no eligible assignees
        # 2. Iterate through unassigned slots in sequence of which is most constrained
        #    Assign staff in order of who gets priority pick at the slot
        # 3. Force for slots that had no eligible after giivng the eligble their voluntary choices
        #    If no forcing availability, label as such and move on
        #End when no more unassigned slots left

        #Proceed with carrying out the algorithm:
        # 1. Initial Forcing
        sls=self.nextSlots(force=1)
        self.assnLog.append('Initial Forcing phase... Forcing to slots with no eligible volunteers')
        for s in sls:
            self.assnLog.append('Looking to Force to '+s.dispNm+' '+ self.slLeg[s.seqID-1][1]+' '+ self.slLeg[s.seqID-1][2])
            eId,tp=self.pickAssignee(s,tp='F')
            s.assn(self,assnType=tp,slAssignee=eId)
        # 2. Voluntary Filling
        self.assnLog.append('Voluntary Assignment Phase... Assigning slots in sequence of most to least constrained by number of eligible volunteers')
        sls=deepcopy(self.slots)
        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'] ]
        #toAssn is the same as the first action in nextSlots().. needed here to iterate the right number of times
        for i in range(len(toAssn)): #Iterate across all slots,
            s=self.nextSlots() #Pick most constrained of all avail slots
            if s is not None:
                self.assnLog.append('Looking to voluntarily assign to '+s.dispNm+' '+ self.slLeg[s.seqID-1][1]+' '+ self.slLeg[s.seqID-1][2])
                # sls.pop(s.key()) #Remove that one from set for next iteration
                eId,tp=self.pickAssignee(s)
                self.slots[s.key()].assn(self,assnType=tp,slAssignee=eId) #Note the assign method acts on original, not deepcopy, retrieved via key        
                # print(str(i))
                # print(self.slots['8_Labeler'].assnType)
        # 3. Final Forced Filling
        self.assnLog.append('Final Forcing Phase... Forcing to slots with no more eligible volunteers after having assigned voluntary OT')
        sls=self.nextSlots(force=2)
        # print('force slots:')
        # print([x.key() for x in sls])
        for s in sls:
            self.assnLog.append('Forcing Phase 2 on '+s.dispNm+' '+ self.slLeg[s.seqID-1][1]+' '+ self.slLeg[s.seqID-1][2])
            eId,tp=self.pickAssignee(s,tp='F')
            s.assn(self,assnType=tp,slAssignee=eId)


    def printToExcel(self):
        """Print all slot assignments to an excel file for human-readable schedule interpretation"""
        #Define Cell styling function
        def styleCell(cl,clType,s=None,horizMergeLength=0):
                for i in range(horizMergeLength+1):
                    cl=cl.offset(0,i)
                    if clType=='hours':
                        cl.font=pyxl.styles.Font(bold=True,size=16,color="00FFFFFF")
                        cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='00FF0000',end_color='00FF0000')
                        cl.alignment=pyxl.styles.Alignment(horizontal='center')
                        cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thick'),
                        right=pyxl.styles.Side(border_style='thick'),
                        top=pyxl.styles.Side(border_style='thick'),
                        bottom=pyxl.styles.Side(border_style='thick'))
                    elif clType=='shift':
                        cl.font=pyxl.styles.Font(bold=True,size=16)
                        cl.alignment=pyxl.styles.Alignment(horizontal='center')
                        cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thick'),
                        right=pyxl.styles.Side(border_style='thick'),
                        top=pyxl.styles.Side(border_style='thick'),
                        bottom=pyxl.styles.Side(border_style='thick'))
                    elif clType=='day':
                        cl.font=pyxl.styles.Font(bold=True,size=28)
                        cl.alignment=pyxl.styles.Alignment(horizontal='center')
                        cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thick'),
                        right=pyxl.styles.Side(border_style='thick'),
                        top=pyxl.styles.Side(border_style='thick'),
                        bottom=pyxl.styles.Side(border_style='thick'))
                    elif clType=='DNS':
                        cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='B2B2B2',end_color='B2B2B2')
                        cl.font=pyxl.styles.Font(bold=True,size=14)
                        cl.alignment=pyxl.styles.Alignment(horizontal='center')
                        cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
                        right=pyxl.styles.Side(border_style='thin'),
                        top=pyxl.styles.Side(border_style='thin'),
                        bottom=pyxl.styles.Side(border_style='thin'))
                    elif clType=='WWF':
                        cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='00B0F0',end_color='00B0F0')
                        cl.font=pyxl.styles.Font(bold=True,size=14,color="00FFFFFF")
                        cl.alignment=pyxl.styles.Alignment(horizontal='center')
                        cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
                        right=pyxl.styles.Side(border_style='thin'),
                        top=pyxl.styles.Side(border_style='thin'),
                        bottom=pyxl.styles.Side(border_style='thin'))
                    elif clType=='F':
                        cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='CC99FF',end_color='CC99FF')
                        cl.font=pyxl.styles.Font(bold=True,size=14)
                        cl.alignment=pyxl.styles.Alignment(horizontal='center')
                        cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
                        right=pyxl.styles.Side(border_style='thin'),
                        top=pyxl.styles.Side(border_style='thin'),
                        bottom=pyxl.styles.Side(border_style='thin'))
                    elif clType=='N':
                        cl.font=pyxl.styles.Font(bold=True,size=16,color="00FFFFFF")
                        cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='00FF0000',end_color='00FF0000')
                        cl.alignment=pyxl.styles.Alignment(horizontal='center')
                        cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thick'),
                        right=pyxl.styles.Side(border_style='thick'),
                        top=pyxl.styles.Side(border_style='thick'),
                        bottom=pyxl.styles.Side(border_style='thick'))
                    elif clType=='jbNm':
                        cl.font=pyxl.styles.Font(bold=True,size=14)
                        cl.alignment=pyxl.styles.Alignment(horizontal='left')
                        cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
                        right=pyxl.styles.Side(border_style='thin'),
                        top=pyxl.styles.Side(border_style='thin'),
                        bottom=pyxl.styles.Side(border_style='thin'))
                    elif clType=='V': #Note that if slot was forced, it will be purple per above if branch, regardless of shift length
                        if self.ee[s.assignee].totShiftHrs(s,styling=True)==4: #Colour 4 hour shifts yellow
                            cl.font=pyxl.styles.Font(bold=True,size=14)
                            cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='00FFC000',end_color='00FFC000')
                            cl.alignment=pyxl.styles.Alignment(horizontal='center')
                            cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
                            right=pyxl.styles.Side(border_style='thin'),
                            top=pyxl.styles.Side(border_style='thin'),
                            bottom=pyxl.styles.Side(border_style='thin'))
                        elif self.ee[s.assignee].totShiftHrs(s,styling=True)==8: #Leave 8 hrs shift white
                            cl.font=pyxl.styles.Font(bold=False,size=14)
                            cl.alignment=pyxl.styles.Alignment(horizontal='center')
                            cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='00FFFFFF',end_color='00FFFFFF')
                            cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
                            right=pyxl.styles.Side(border_style='thin'),
                            top=pyxl.styles.Side(border_style='thin'),
                            bottom=pyxl.styles.Side(border_style='thin'))
                        elif self.ee[s.assignee].totShiftHrs(s,styling=True)==12: #Colour 12 hrs shift green
                            cl.font=pyxl.styles.Font(bold=True,size=14)   
                            cl.alignment=pyxl.styles.Alignment(horizontal='center')
                            cl.fill=pyxl.styles.PatternFill(fill_type="solid",start_color='0092D050',end_color='0092D050')
                            cl.border=pyxl.styles.Border(left=pyxl.styles.Side(border_style='thin'),
                            right=pyxl.styles.Side(border_style='thin'),
                            top=pyxl.styles.Side(border_style='thin'),
                            bottom=pyxl.styles.Side(border_style='thin'))
                            
        #Initial Setup
        self.rev+=1
        wb=pyxl.Workbook()
        dest_filename = 'Wknd Sched_Rev '+str(self.rev)+'.xlsx'
        ws = wb.active
        ws.title = "Full Schedule" #Title worksheet for printout
        ws['A1']='Rev '+str(self.rev)
        styleCell(ws['A1'],'shift')
        ws['A2']='Manager'
        styleCell(ws['A2'],'shift')
        ws.column_dimensions['A'].width =22.78
        dys=['Friday','Saturday','Sunday','Monday']
        shifts=['C','A','B']*4
        tSlots=['11p - 3a','3a - 7a','7a - 11a', '11a - 3p','3p-7p', '7p-11p']*4
        for i in range(0,24,6): #Print Days of week
            cl=ws.cell(column=2+i,row=1)
            styleCell(cl,'day',6) #Style all the cells within the merge
            ws.merge_cells(start_row=1, start_column=2+i, end_row=1, end_column=2+i+5)
            cl.value=dys.pop(0)
        for i in range(0,24,2): #Print the shift title
            cl=ws.cell(column=2+i,row=3)
            styleCell(cl,'shift',1)
            ws.merge_cells(start_row=3, start_column=2+i, end_row=3, end_column=2+i+1)
            cl.value=shifts.pop(0)
        for i in range(24): #Print the shift times row
            cl=ws.cell(column=2+i,row=4)
            cl.value=tSlots.pop(0)
            styleCell(cl,'hours')

        def styleNfill(cl,s):
            if s.assnType=='DNS': cl.value="N/A"
            elif s.assignee is not None:
                val=self.ee[s.assignee].dispNm()
                if s.assnType=='F': val=val+'(F)' #Append force identifier
                cl.value=val
            else: 
                s.assnType='N'
                cl.value='NO STAFF'
            styleCell(cl,s.assnType,s)

        #==================
        #Enter shifts
        #The sequence of keys returned from self.slots.keys() is the same as the slots required were defined in "AllSlots"
        #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
        #And they are not in order after all.
        #The method here is to use a dictionary keyed by job name to track which row a job is printed out to,
        #and to add a job to the dictionary if/when it is encountered for the first time.
        jrD={} #job/Row Dict {job:row}
        r=5 #Start printing job slot info's at row 5 in excel
        for k in self.slots:
            s=self.slots[k] #Retrieve slot
            if s.dispNm not in jrD: #jobRowDict - Add to dict if first time seeing a job
                jrD[s.dispNm]=r
                jbCl=ws.cell(row=r,column=1)
                jbCl.value=s.dispNm
                styleCell(jbCl,'jbNm')
                r+=1 #increment for next one to be observed
            cl=ws.cell(row=jrD[s.dispNm],column=1+s.seqID)
            styleNfill(cl,s)
        #=========================================
        #Go through the schedule to add the (1/3),(1/2),(2/2) etc etc etc slot identifiers for human readibility
        #Define helper function
        def ranges(nums):
            """Returns a list of (open,close) intervals a list spans"""
            nums = sorted(set(nums))
            gaps = [[s, e] for s, e in zip(nums, nums[1:]) if s+1 < e]
            edges = iter(nums[:1] + sum(gaps, []) + nums[-1:])
            return list(zip(edges, edges))
        #===================
        #Proceed with helper function
        for k in self.slots:
            s=self.slots[k] #Retrieve slot
            if s.assignee!=None:
                ids=sorted([self.slots[k].seqID for k in self.ee[s.assignee].assignments])
                rn=ranges(ids)#sets of start-end intervals
                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
                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
                contents= [[s.dispNm,s.assnType] for s in mysls] #pull slots jobs/assn.Type
                denom=1
                for i in range(len(contents)-1):
                    if contents[i]!=contents[i+1]:
                        denom+=1
                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
                x=ws.cell(row=jrD[s.dispNm],column=1+s.seqID).value
                if denom!=1:
                    ws.cell(row=jrD[s.dispNm],column=1+s.seqID).value=str(x)+' ('+str(numrtr)+'/'+str(denom)+')'
        #======================================================
        #Merge contiguous shifts cells
        #- Define a custom function to facilitate it
        def numInARow(cl,n=1):
            """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"""
            nextval=cl.offset(0,1).value
            if nextval is None or nextval=='':
                return n
            elif cl.value!='' and cl.value is not None:
                if '/' in nextval:
                    nextval=cl.offset(0,1).value[:cl.offset(0,1).value.index('/')-3]
                curVal=cl.value
                if '/' in cl.value:
                    curVal=cl.value[:cl.value.index('/')-3]
                if nextval==curVal:
                    return numInARow(cl.offset(0,1),n+1) #Recursive fn. If next cell matches current, use this function on the next one again
                else: return n
            else: return n
        #====== Proceed with above function to be used
        nSkip=0 #Initialize for skipping cells in this loop as applicable
        for rw in range(5,r):
            for i in range(2,26):
                if nSkip<1:
                    inArow=numInARow(ws.cell(row=rw,column=i))
                    nSkip+=(inArow-1)
                    ws.merge_cells(start_row=rw, start_column=i, end_row=rw, end_column=i+inArow-1)
                elif nSkip>0: 
                    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
        #======================================
        #Format column widths
        #Doesnt appear to be working for some reason :(  
        for k in self.slots:
            s=self.slots[k] #Retrieve slot
            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
        #=============================================
        #Print all forcings
        ws3 = wb.create_sheet(title="FORCINGS")
        ws3.cell(row=1,column=1).value='Employee ID'
        ws3.cell(row=1,column=2).value='Time slot'
        c=0
        for k in [k for k in self.slots if self.slots[k].assnType=='F']:
            ws3.cell(row=2+c,column=1).value=self.ee[self.slots[k].assignee].dispNm()
            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]+')'
            c+=1
        #=============================================
        #Print assignments to a separate sheet, in alphabetical order by last name
        ws = wb.create_sheet(title="Assignments (Alpha)")
        ws.cell(row=3,column=1).value='Last, First'
        ws.cell(row=2,column=2).value='Time slots'
        styleCell(ws['A3'],'shift')
        # styleCell(ws['A2'],'shift')
        ws.column_dimensions['A'].width =22.78
        dys=['Friday','Saturday','Sunday','Monday']
        shifts=['C','A','B']*4
        tSlots=['11p - 3a','3a - 7a','7a - 11a', '11a - 3p','3p-7p', '7p-11p']*4
        for i in range(0,24,6): #Print Days of week
            cl=ws.cell(column=2+i,row=1)
            styleCell(cl,'day',6) #Style all the cells within the merge
            ws.merge_cells(start_row=1, start_column=2+i, end_row=1, end_column=2+i+5)
            cl.value=dys.pop(0)
        for i in range(0,24,2): #Print the shift title
            cl=ws.cell(column=2+i,row=2)
            styleCell(cl,'shift',1)
            ws.merge_cells(start_row=2, start_column=2+i, end_row=2, end_column=2+i+1)
            cl.value=shifts.pop(0)
        for i in range(24): #Print the shift times row
            cl=ws.cell(column=2+i,row=3)
            cl.value=tSlots.pop(0)
            styleCell(cl,'hours')
        # ws2.cell(row=1,column=1).value='Note that the seniority value presented is not actual plant seniority number, but just the sequence of '
        n=3
        for rec in tls.viewTBL('senRef',sortBy=[('last','ASC')]):
            eId=rec[2]
            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
                n+=1
                ws.cell(row=n,column=1).value=self.ee[eId].lastNm+', '+self.ee[eId].firstNm[0]+'.' #Print name in column A
                #Print N/A to all other cells, actual assignments will overwrite.
                for i in range(2,26):
                    styleCell(ws.cell(row=n,column=i),'DNS')
                    ws.cell(row=n,column=i).value="-"
                c=0
                for k in sorted(self.ee[eId].assignments,key=lambda k:int(k[:k.index('_')])):
                    styleNfill(ws.cell(row=n,column=1+int(k[:k.index('_')])),self.slots[k])
                    # 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]+')'
                    ws.cell(row=n,column=1+int(k[:k.index('_')])).value=self.slots[k].dispNm
                    c+=1
        #==========================
        #Print succint assignment log to a separate sheet
        ws2 = wb.create_sheet(title="Succint Ass'n Log")
        ws2.cell(row=1,column=1).value="This data can be used on Tab C of the schedule building web app to investigate what a schedule looked like mid-generation"
        ws2.cell(row=2,column=1).value='Assn #'
        ws2.cell(row=2,column=2).value='EE Nm'
        ws2.cell(row=2,column=3).value='Slot #_Job Nm'
        ws2.cell(row=2,column=4).value='Assignment Type'
        n=1
        for rec in self.aOnly:
            ws2.cell(column=1,row=2+n).value=rec[0]
            ws2.cell(column=2,row=2+n).value=rec[1]
            ws2.cell(column=3,row=2+n).value=rec[2]
            ws2.cell(column=4,row=2+n).value=rec[3]
            n+=1
        #==========================
        #Print verbose assignment log to a separate sheet
        ws2 = wb.create_sheet(title="Verbose Ass'n Log")
        ws2.cell(row=2,column=1).value='List of assignment decisions made throughout scheduling process'
        # ws2.cell(row=1,column=1).value='Note that the seniority value presented is not actual plant seniority number, but just the sequence of '
        n=1
        for rec in self.assnLog:
            ws2.cell(column=1,row=2+n).value=rec
            n+=1

        #==========================================================
        #Print assignments to a separate sheet, sequenced by seniority
        ws2 = wb.create_sheet(title="Assignments (Sen'ty)")
        ws2.cell(row=2,column=1).value='Seniority'
        ws2.cell(row=2,column=2).value='Employee ID'
        ws2.cell(row=2,column=3).value='Time slots'
        # ws2.cell(row=1,column=1).value='Note that the seniority value presented is not actual plant seniority number, but just the sequence of '
        n=1
        for i in range(len(self.senList)-1):
            eId=self.senList[i][2]
            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
                n+=1
                ws2.cell(row=n+1,column=2).value=self.senList[i][0]
                ws2.cell(row=n+1,column=2).value=eId
                c=0
                for k in sorted(self.ee[eId].assignments,key=lambda k:int(k[:k.index('_')])):
                    styleNfill(ws2.cell(row=n+1,column=3+c),self.slots[k])
                    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]+')'
                    c+=1
        #====================================================
               


        wb.save(filename = dest_filename)
        return dest_filename