vkk.workhours.accounting.projects.project.export.receipts.forms
1from django import forms 2from decimal import Decimal 3from django.db.models import Sum, F 4from django.core.exceptions import ValidationError 5from django.core.serializers.json import DjangoJSONEncoder 6from django.utils.translation import gettext_lazy as _ 7from vkk.workhours import models 8from vkk.generic.forms import CustomDateInput 9 10 11class ReceiptForm(forms.ModelForm): 12 class Meta: 13 model = models.Receipt 14 fields = ['start', 'end', 'receipt_number', 'buper'] 15 widgets = { 16 'start': CustomDateInput, 17 'end': CustomDateInput, 18 } 19 20 class Media: 21 js = ('scripts/receipts.js',) 22 23 def __init__(self, *args, project=None, **kwargs): 24 super().__init__(*args, **kwargs) 25 self.project = project 26 self.department = self.project.department 27 self.general_costs = None 28 self.department_costs = None 29 self.project_funded_staff_date = None 30 self.project_funded_staff = None 31 self.salary_level_date = None 32 self.salary_costs = None 33 self.salary_costs_annotated1 = None 34 self.salary_costs_annotated2 = None 35 self.data_dict = None 36 37 def set_and_clean_general_costs(self, start, end): 38 if models.GeneralCosts.objects.filter(start__gt=start, start__lte=end,).exists(): 39 raise ValidationError( 40 _('General cost records are ambiguous.'), 41 code='ambiguous_general_costs' 42 ) 43 try: 44 self.general_costs = models.GeneralCosts.objects.filter( 45 start__lte=start 46 ).latest('start') 47 except models.GeneralCosts.DoesNotExist: 48 raise ValidationError( 49 _('No valid general cost record found'), 50 code='no_general_costs' 51 ) 52 53 def set_and_clean_department_costs(self, start, end): 54 if models.DepartmentCosts.objects.filter( 55 department=self.department, 56 start__date__gt=start, 57 start__date__lte=end, 58 ).exists(): 59 raise ValidationError( 60 _('Department cost records are ambiguous.'), 61 code='ambiguous_department_costs' 62 ) 63 try: 64 self.department_costs = models.DepartmentCosts.objects.filter( 65 department=self.department, 66 start__date__lte=start, 67 ).latest('start__date') 68 except models.DepartmentCosts.DoesNotExist: 69 raise ValidationError( 70 _('No valid department cost record found'), 71 code='no_department_costs' 72 ) 73 74 def set_and_clean_project_funded_staff(self, start, end): 75 if models.ProjectFundedStaffDate.objects.filter( 76 project=self.project, 77 date__gt=start, 78 date__lte=end, 79 ).exists(): 80 raise ValidationError( 81 _('Project funded staff records are ambiguous.'), 82 code='ambiguous_staff_costs' 83 ) 84 try: 85 self.project_funded_staff_date = models.ProjectFundedStaffDate.objects.filter( 86 project=self.project, 87 date__lte=start, 88 ).latest('date') 89 except models.ProjectFundedStaffDate.DoesNotExist: 90 # This is valid behaviour, no exception needed 91 pass 92 if self.project_funded_staff_date is not None: 93 self.project_funded_staff = models.ProjectFundedStaff.objects.filter( 94 start=self.project_funded_staff_date 95 ) 96 97 def set_and_clean_salary_level(self, start, end): 98 if models.SalaryLevelDate.objects.filter( 99 date__gt=start, 100 date__lte=end, 101 ).exists(): 102 raise ValidationError( 103 _('Salary Level records are ambiguous.'), 104 code='ambiguous_salary_costs' 105 ) 106 try: 107 self.salary_level_date = models.SalaryLevelDate.objects.filter( 108 date__lte=start 109 ).latest('date') 110 except: 111 raise ValidationError( 112 _('No valid salary level cost records found'), 113 code='no_salary_costs' 114 ) 115 if self.salary_level_date is not None: 116 self.salary_costs = models.SalaryLevelCosts.objects.filter( 117 start=self.salary_level_date 118 ) 119 120 def check_peroid_overlap(self, start, end): 121 # check for matching period 122 if not models.Period.objects.filter(start=start, end=end).exists(): 123 raise ValidationError( 124 _('End and Start do not match up with given periods'), 125 code='period_ambiguous' 126 ) 127 128 def check_closed_periods(self, start, end): 129 assignments_not_closed = models.ProjectAssignment.objects.filter( 130 project=self.project 131 ).exclude( 132 periodclosure__period__start=start, 133 periodclosure__period__end=end, 134 periodclosure__is_closed_manager=True, 135 periodclosure__is_closed_contributor=True, 136 ) 137 if assignments_not_closed.exists(): 138 raise ValidationError( 139 _('Some contributors or project managers did not close their work hour inputs'), 140 code='periods_not_closed' 141 ) 142 143 def set_and_clean_workhours(self, start, end): 144 if self.salary_costs is not None: 145 # Specify constraints for aggregations 146 agg1 = self.salary_costs.filter( 147 salary_level__projectassignment__project=self.project, 148 salary_level__projectassignment__workhours__day__gte=start, 149 salary_level__projectassignment__workhours__day__lte=end, 150 ) 151 # Annotate work hours 152 agg1 = agg1.annotate( 153 workhours=Sum( 154 'salary_level__projectassignment__workhours__hours') 155 ) 156 agg1 = agg1.annotate( 157 costs=F('brutto_per_hour') * 158 Sum('salary_level__projectassignment__workhours__hours') 159 ) 160 161 self.salary_costs_annotated1 = agg1 162 163 agg2 = self.salary_costs.filter( 164 salary_level__projectassignment__project=self.project, 165 salary_level__projectassignment__workhourscorrection__period__start=start, 166 salary_level__projectassignment__workhourscorrection__period__end=end, 167 ) 168 agg2 = agg2.annotate( 169 workhours_correction=Sum( 170 'salary_level__projectassignment__workhourscorrection__ammount' 171 ) 172 ) 173 agg2 = agg2.annotate( 174 costs=F('brutto_per_hour') * 175 Sum('salary_level__projectassignment__workhourscorrection__ammount') 176 ) 177 178 self.salary_costs_annotated2 = agg2 179 180 def clean(self): 181 start = self.cleaned_data.get('start') 182 end = self.cleaned_data.get('end') 183 if start is not None and end is not None: 184 self.set_and_clean_general_costs(start, end) 185 self.set_and_clean_department_costs(start, end) 186 self.set_and_clean_project_funded_staff(start, end) 187 self.set_and_clean_salary_level(start, end) 188 self.check_peroid_overlap(start, end) 189 self.check_closed_periods(start, end) 190 self.set_and_clean_workhours(start, end) 191 return self.cleaned_data 192 193 def save(self, commit=True): 194 self.instance.project = self.project 195 self.instance.data = self.to_json() 196 return super().save(commit) 197 198 def _project_dict(self): 199 return { 200 'project': { 201 'invoice_number': self.project.invoice_number, 202 'name': self.project.name, 203 'contractor': self.project.contractor, 204 'start': self.project.start, 205 'end': self.project.end, 206 } 207 } 208 209 def _department_dict(self): 210 return { 211 'department': { 212 'name': self.department.name, 213 'accounting_entry': self.department.accounting_entry, 214 'invoice_number': self.department.invoice_number, 215 } 216 } 217 218 def _general_costs_dict(self): 219 return { 220 'general_costs': { 221 'start': self.general_costs.start, 222 'costs': self.general_costs.costs, 223 } 224 } 225 226 def _department_costs_dict(self): 227 return { 228 'department_costs': { 229 'start': self.department_costs.start.date, 230 'equivalents_per_hour': self.department_costs.equivalents_per_hour, 231 } 232 } 233 234 def _project_funded_staff_dict(self): 235 return { 236 'project_funded_staff': { 237 'start': self.project_funded_staff_date.date if 238 self.project_funded_staff_date is not None else None, 239 'hours_by_salary_level': { 240 entry.salary_level.salary_code: { 241 'hours': entry.hours, 242 'brutto_per_hour': self.salary_costs.get( 243 salary_level=entry.salary_level 244 ).brutto_per_hour, 245 } for entry in self.project_funded_staff or [] 246 }, 247 'hours_sum': sum([ 248 entry.hours for entry in self.project_funded_staff or [] 249 ]), 250 } 251 } 252 253 def _salary_costs_annotated_dict(self): 254 summed_costs = [] 255 256 for entry in self.salary_costs: 257 workhours, costs, code, brutto = None, None, None, None 258 annotated1 = self.salary_costs_annotated1.filter(id=entry.id) 259 if annotated1.exists(): 260 code = annotated1[0].salary_level.salary_code 261 brutto = annotated1[0].brutto_per_hour 262 workhours = annotated1[0].workhours 263 costs = annotated1[0].costs 264 265 annotated2 = self.salary_costs_annotated2.filter(id=entry.id) 266 if annotated2.exists(): 267 code = annotated2[0].salary_level.salary_code 268 brutto = annotated2[0].brutto_per_hour 269 if workhours is None: 270 workhours = 0 271 if costs is None: 272 costs = 0 273 workhours += annotated2[0].workhours_correction 274 costs += annotated2[0].costs 275 276 if workhours is not None and costs is not None \ 277 and code is not None and brutto is not None: 278 summed_costs.append((workhours, costs, code, brutto)) 279 280 return { 281 'salary_costs_annotated': { 282 'start': self.salary_level_date.date, 283 'salary_levels': { 284 entry[2]: { 285 'brutto_per_hour': entry[3], 286 'hours': entry[0], 287 'costs': entry[1], 288 } for entry in summed_costs 289 }, 290 'hours_sum': sum([ 291 entry[0] for entry in summed_costs 292 ]), 293 'costs_sum': sum([ 294 entry[1] for entry in summed_costs 295 ]), 296 } 297 } 298 299 def to_data_dict(self): 300 self.data_dict = self._project_dict() \ 301 | self._department_dict() \ 302 | self._general_costs_dict() \ 303 | self._department_costs_dict() \ 304 | self._project_funded_staff_dict() \ 305 | self._salary_costs_annotated_dict() 306 307 self.data_dict['department_costs'].update( 308 { 309 'salary_costs': self.department_costs.equivalents_per_hour 310 * (self.data_dict['project_funded_staff']['hours_sum'] 311 + self.data_dict['salary_costs_annotated']['hours_sum']), 312 } 313 ) 314 self.data_dict['general_costs'].update( 315 { 316 'total': (self.data_dict['project_funded_staff']['hours_sum'] 317 + self.data_dict['salary_costs_annotated']['hours_sum']) 318 * self.data_dict['general_costs']['costs'], 319 } 320 ) 321 322 return self.data_dict 323 324 def to_json(self): 325 return CostumJSONEncoder().encode(self.to_data_dict()) 326 327 328class CostumJSONEncoder(DjangoJSONEncoder): 329 def default(self, o): 330 if isinstance(o, Decimal): 331 return str(round(o, 2)).replace(".", ",") 332 else: 333 return super().default(o) 334 335 336class ReceiptTemplateSelectForm(forms.Form): 337 def __init__(self, *args, **kwargs): 338 super().__init__(*args, **kwargs) 339 queryset = models.ReceiptTemplate.objects.all().order_by('-start') 340 self.fields["receipt_template"] = forms.ModelChoiceField( 341 queryset, 342 empty_label=None, 343 label=_('Receipt Template') 344 )
12class ReceiptForm(forms.ModelForm): 13 class Meta: 14 model = models.Receipt 15 fields = ['start', 'end', 'receipt_number', 'buper'] 16 widgets = { 17 'start': CustomDateInput, 18 'end': CustomDateInput, 19 } 20 21 class Media: 22 js = ('scripts/receipts.js',) 23 24 def __init__(self, *args, project=None, **kwargs): 25 super().__init__(*args, **kwargs) 26 self.project = project 27 self.department = self.project.department 28 self.general_costs = None 29 self.department_costs = None 30 self.project_funded_staff_date = None 31 self.project_funded_staff = None 32 self.salary_level_date = None 33 self.salary_costs = None 34 self.salary_costs_annotated1 = None 35 self.salary_costs_annotated2 = None 36 self.data_dict = None 37 38 def set_and_clean_general_costs(self, start, end): 39 if models.GeneralCosts.objects.filter(start__gt=start, start__lte=end,).exists(): 40 raise ValidationError( 41 _('General cost records are ambiguous.'), 42 code='ambiguous_general_costs' 43 ) 44 try: 45 self.general_costs = models.GeneralCosts.objects.filter( 46 start__lte=start 47 ).latest('start') 48 except models.GeneralCosts.DoesNotExist: 49 raise ValidationError( 50 _('No valid general cost record found'), 51 code='no_general_costs' 52 ) 53 54 def set_and_clean_department_costs(self, start, end): 55 if models.DepartmentCosts.objects.filter( 56 department=self.department, 57 start__date__gt=start, 58 start__date__lte=end, 59 ).exists(): 60 raise ValidationError( 61 _('Department cost records are ambiguous.'), 62 code='ambiguous_department_costs' 63 ) 64 try: 65 self.department_costs = models.DepartmentCosts.objects.filter( 66 department=self.department, 67 start__date__lte=start, 68 ).latest('start__date') 69 except models.DepartmentCosts.DoesNotExist: 70 raise ValidationError( 71 _('No valid department cost record found'), 72 code='no_department_costs' 73 ) 74 75 def set_and_clean_project_funded_staff(self, start, end): 76 if models.ProjectFundedStaffDate.objects.filter( 77 project=self.project, 78 date__gt=start, 79 date__lte=end, 80 ).exists(): 81 raise ValidationError( 82 _('Project funded staff records are ambiguous.'), 83 code='ambiguous_staff_costs' 84 ) 85 try: 86 self.project_funded_staff_date = models.ProjectFundedStaffDate.objects.filter( 87 project=self.project, 88 date__lte=start, 89 ).latest('date') 90 except models.ProjectFundedStaffDate.DoesNotExist: 91 # This is valid behaviour, no exception needed 92 pass 93 if self.project_funded_staff_date is not None: 94 self.project_funded_staff = models.ProjectFundedStaff.objects.filter( 95 start=self.project_funded_staff_date 96 ) 97 98 def set_and_clean_salary_level(self, start, end): 99 if models.SalaryLevelDate.objects.filter( 100 date__gt=start, 101 date__lte=end, 102 ).exists(): 103 raise ValidationError( 104 _('Salary Level records are ambiguous.'), 105 code='ambiguous_salary_costs' 106 ) 107 try: 108 self.salary_level_date = models.SalaryLevelDate.objects.filter( 109 date__lte=start 110 ).latest('date') 111 except: 112 raise ValidationError( 113 _('No valid salary level cost records found'), 114 code='no_salary_costs' 115 ) 116 if self.salary_level_date is not None: 117 self.salary_costs = models.SalaryLevelCosts.objects.filter( 118 start=self.salary_level_date 119 ) 120 121 def check_peroid_overlap(self, start, end): 122 # check for matching period 123 if not models.Period.objects.filter(start=start, end=end).exists(): 124 raise ValidationError( 125 _('End and Start do not match up with given periods'), 126 code='period_ambiguous' 127 ) 128 129 def check_closed_periods(self, start, end): 130 assignments_not_closed = models.ProjectAssignment.objects.filter( 131 project=self.project 132 ).exclude( 133 periodclosure__period__start=start, 134 periodclosure__period__end=end, 135 periodclosure__is_closed_manager=True, 136 periodclosure__is_closed_contributor=True, 137 ) 138 if assignments_not_closed.exists(): 139 raise ValidationError( 140 _('Some contributors or project managers did not close their work hour inputs'), 141 code='periods_not_closed' 142 ) 143 144 def set_and_clean_workhours(self, start, end): 145 if self.salary_costs is not None: 146 # Specify constraints for aggregations 147 agg1 = self.salary_costs.filter( 148 salary_level__projectassignment__project=self.project, 149 salary_level__projectassignment__workhours__day__gte=start, 150 salary_level__projectassignment__workhours__day__lte=end, 151 ) 152 # Annotate work hours 153 agg1 = agg1.annotate( 154 workhours=Sum( 155 'salary_level__projectassignment__workhours__hours') 156 ) 157 agg1 = agg1.annotate( 158 costs=F('brutto_per_hour') * 159 Sum('salary_level__projectassignment__workhours__hours') 160 ) 161 162 self.salary_costs_annotated1 = agg1 163 164 agg2 = self.salary_costs.filter( 165 salary_level__projectassignment__project=self.project, 166 salary_level__projectassignment__workhourscorrection__period__start=start, 167 salary_level__projectassignment__workhourscorrection__period__end=end, 168 ) 169 agg2 = agg2.annotate( 170 workhours_correction=Sum( 171 'salary_level__projectassignment__workhourscorrection__ammount' 172 ) 173 ) 174 agg2 = agg2.annotate( 175 costs=F('brutto_per_hour') * 176 Sum('salary_level__projectassignment__workhourscorrection__ammount') 177 ) 178 179 self.salary_costs_annotated2 = agg2 180 181 def clean(self): 182 start = self.cleaned_data.get('start') 183 end = self.cleaned_data.get('end') 184 if start is not None and end is not None: 185 self.set_and_clean_general_costs(start, end) 186 self.set_and_clean_department_costs(start, end) 187 self.set_and_clean_project_funded_staff(start, end) 188 self.set_and_clean_salary_level(start, end) 189 self.check_peroid_overlap(start, end) 190 self.check_closed_periods(start, end) 191 self.set_and_clean_workhours(start, end) 192 return self.cleaned_data 193 194 def save(self, commit=True): 195 self.instance.project = self.project 196 self.instance.data = self.to_json() 197 return super().save(commit) 198 199 def _project_dict(self): 200 return { 201 'project': { 202 'invoice_number': self.project.invoice_number, 203 'name': self.project.name, 204 'contractor': self.project.contractor, 205 'start': self.project.start, 206 'end': self.project.end, 207 } 208 } 209 210 def _department_dict(self): 211 return { 212 'department': { 213 'name': self.department.name, 214 'accounting_entry': self.department.accounting_entry, 215 'invoice_number': self.department.invoice_number, 216 } 217 } 218 219 def _general_costs_dict(self): 220 return { 221 'general_costs': { 222 'start': self.general_costs.start, 223 'costs': self.general_costs.costs, 224 } 225 } 226 227 def _department_costs_dict(self): 228 return { 229 'department_costs': { 230 'start': self.department_costs.start.date, 231 'equivalents_per_hour': self.department_costs.equivalents_per_hour, 232 } 233 } 234 235 def _project_funded_staff_dict(self): 236 return { 237 'project_funded_staff': { 238 'start': self.project_funded_staff_date.date if 239 self.project_funded_staff_date is not None else None, 240 'hours_by_salary_level': { 241 entry.salary_level.salary_code: { 242 'hours': entry.hours, 243 'brutto_per_hour': self.salary_costs.get( 244 salary_level=entry.salary_level 245 ).brutto_per_hour, 246 } for entry in self.project_funded_staff or [] 247 }, 248 'hours_sum': sum([ 249 entry.hours for entry in self.project_funded_staff or [] 250 ]), 251 } 252 } 253 254 def _salary_costs_annotated_dict(self): 255 summed_costs = [] 256 257 for entry in self.salary_costs: 258 workhours, costs, code, brutto = None, None, None, None 259 annotated1 = self.salary_costs_annotated1.filter(id=entry.id) 260 if annotated1.exists(): 261 code = annotated1[0].salary_level.salary_code 262 brutto = annotated1[0].brutto_per_hour 263 workhours = annotated1[0].workhours 264 costs = annotated1[0].costs 265 266 annotated2 = self.salary_costs_annotated2.filter(id=entry.id) 267 if annotated2.exists(): 268 code = annotated2[0].salary_level.salary_code 269 brutto = annotated2[0].brutto_per_hour 270 if workhours is None: 271 workhours = 0 272 if costs is None: 273 costs = 0 274 workhours += annotated2[0].workhours_correction 275 costs += annotated2[0].costs 276 277 if workhours is not None and costs is not None \ 278 and code is not None and brutto is not None: 279 summed_costs.append((workhours, costs, code, brutto)) 280 281 return { 282 'salary_costs_annotated': { 283 'start': self.salary_level_date.date, 284 'salary_levels': { 285 entry[2]: { 286 'brutto_per_hour': entry[3], 287 'hours': entry[0], 288 'costs': entry[1], 289 } for entry in summed_costs 290 }, 291 'hours_sum': sum([ 292 entry[0] for entry in summed_costs 293 ]), 294 'costs_sum': sum([ 295 entry[1] for entry in summed_costs 296 ]), 297 } 298 } 299 300 def to_data_dict(self): 301 self.data_dict = self._project_dict() \ 302 | self._department_dict() \ 303 | self._general_costs_dict() \ 304 | self._department_costs_dict() \ 305 | self._project_funded_staff_dict() \ 306 | self._salary_costs_annotated_dict() 307 308 self.data_dict['department_costs'].update( 309 { 310 'salary_costs': self.department_costs.equivalents_per_hour 311 * (self.data_dict['project_funded_staff']['hours_sum'] 312 + self.data_dict['salary_costs_annotated']['hours_sum']), 313 } 314 ) 315 self.data_dict['general_costs'].update( 316 { 317 'total': (self.data_dict['project_funded_staff']['hours_sum'] 318 + self.data_dict['salary_costs_annotated']['hours_sum']) 319 * self.data_dict['general_costs']['costs'], 320 } 321 ) 322 323 return self.data_dict 324 325 def to_json(self): 326 return CostumJSONEncoder().encode(self.to_data_dict())
The main implementation of all the Form logic. Note that this class is different than Form. See the comments by the Form class for more info. Any improvements to the form API should be made to this class, not to the Form class.
24 def __init__(self, *args, project=None, **kwargs): 25 super().__init__(*args, **kwargs) 26 self.project = project 27 self.department = self.project.department 28 self.general_costs = None 29 self.department_costs = None 30 self.project_funded_staff_date = None 31 self.project_funded_staff = None 32 self.salary_level_date = None 33 self.salary_costs = None 34 self.salary_costs_annotated1 = None 35 self.salary_costs_annotated2 = None 36 self.data_dict = None
38 def set_and_clean_general_costs(self, start, end): 39 if models.GeneralCosts.objects.filter(start__gt=start, start__lte=end,).exists(): 40 raise ValidationError( 41 _('General cost records are ambiguous.'), 42 code='ambiguous_general_costs' 43 ) 44 try: 45 self.general_costs = models.GeneralCosts.objects.filter( 46 start__lte=start 47 ).latest('start') 48 except models.GeneralCosts.DoesNotExist: 49 raise ValidationError( 50 _('No valid general cost record found'), 51 code='no_general_costs' 52 )
54 def set_and_clean_department_costs(self, start, end): 55 if models.DepartmentCosts.objects.filter( 56 department=self.department, 57 start__date__gt=start, 58 start__date__lte=end, 59 ).exists(): 60 raise ValidationError( 61 _('Department cost records are ambiguous.'), 62 code='ambiguous_department_costs' 63 ) 64 try: 65 self.department_costs = models.DepartmentCosts.objects.filter( 66 department=self.department, 67 start__date__lte=start, 68 ).latest('start__date') 69 except models.DepartmentCosts.DoesNotExist: 70 raise ValidationError( 71 _('No valid department cost record found'), 72 code='no_department_costs' 73 )
75 def set_and_clean_project_funded_staff(self, start, end): 76 if models.ProjectFundedStaffDate.objects.filter( 77 project=self.project, 78 date__gt=start, 79 date__lte=end, 80 ).exists(): 81 raise ValidationError( 82 _('Project funded staff records are ambiguous.'), 83 code='ambiguous_staff_costs' 84 ) 85 try: 86 self.project_funded_staff_date = models.ProjectFundedStaffDate.objects.filter( 87 project=self.project, 88 date__lte=start, 89 ).latest('date') 90 except models.ProjectFundedStaffDate.DoesNotExist: 91 # This is valid behaviour, no exception needed 92 pass 93 if self.project_funded_staff_date is not None: 94 self.project_funded_staff = models.ProjectFundedStaff.objects.filter( 95 start=self.project_funded_staff_date 96 )
98 def set_and_clean_salary_level(self, start, end): 99 if models.SalaryLevelDate.objects.filter( 100 date__gt=start, 101 date__lte=end, 102 ).exists(): 103 raise ValidationError( 104 _('Salary Level records are ambiguous.'), 105 code='ambiguous_salary_costs' 106 ) 107 try: 108 self.salary_level_date = models.SalaryLevelDate.objects.filter( 109 date__lte=start 110 ).latest('date') 111 except: 112 raise ValidationError( 113 _('No valid salary level cost records found'), 114 code='no_salary_costs' 115 ) 116 if self.salary_level_date is not None: 117 self.salary_costs = models.SalaryLevelCosts.objects.filter( 118 start=self.salary_level_date 119 )
129 def check_closed_periods(self, start, end): 130 assignments_not_closed = models.ProjectAssignment.objects.filter( 131 project=self.project 132 ).exclude( 133 periodclosure__period__start=start, 134 periodclosure__period__end=end, 135 periodclosure__is_closed_manager=True, 136 periodclosure__is_closed_contributor=True, 137 ) 138 if assignments_not_closed.exists(): 139 raise ValidationError( 140 _('Some contributors or project managers did not close their work hour inputs'), 141 code='periods_not_closed' 142 )
144 def set_and_clean_workhours(self, start, end): 145 if self.salary_costs is not None: 146 # Specify constraints for aggregations 147 agg1 = self.salary_costs.filter( 148 salary_level__projectassignment__project=self.project, 149 salary_level__projectassignment__workhours__day__gte=start, 150 salary_level__projectassignment__workhours__day__lte=end, 151 ) 152 # Annotate work hours 153 agg1 = agg1.annotate( 154 workhours=Sum( 155 'salary_level__projectassignment__workhours__hours') 156 ) 157 agg1 = agg1.annotate( 158 costs=F('brutto_per_hour') * 159 Sum('salary_level__projectassignment__workhours__hours') 160 ) 161 162 self.salary_costs_annotated1 = agg1 163 164 agg2 = self.salary_costs.filter( 165 salary_level__projectassignment__project=self.project, 166 salary_level__projectassignment__workhourscorrection__period__start=start, 167 salary_level__projectassignment__workhourscorrection__period__end=end, 168 ) 169 agg2 = agg2.annotate( 170 workhours_correction=Sum( 171 'salary_level__projectassignment__workhourscorrection__ammount' 172 ) 173 ) 174 agg2 = agg2.annotate( 175 costs=F('brutto_per_hour') * 176 Sum('salary_level__projectassignment__workhourscorrection__ammount') 177 ) 178 179 self.salary_costs_annotated2 = agg2
181 def clean(self): 182 start = self.cleaned_data.get('start') 183 end = self.cleaned_data.get('end') 184 if start is not None and end is not None: 185 self.set_and_clean_general_costs(start, end) 186 self.set_and_clean_department_costs(start, end) 187 self.set_and_clean_project_funded_staff(start, end) 188 self.set_and_clean_salary_level(start, end) 189 self.check_peroid_overlap(start, end) 190 self.check_closed_periods(start, end) 191 self.set_and_clean_workhours(start, end) 192 return self.cleaned_data
Hook for doing any extra form-wide cleaning after Field.clean() has been called on every field. Any ValidationError raised by this method will not be associated with a particular field; it will have a special-case association with the field named '__all__'.
194 def save(self, commit=True): 195 self.instance.project = self.project 196 self.instance.data = self.to_json() 197 return super().save(commit)
Save this form's self.instance object if commit=True. Otherwise, add a save_m2m() method to the form which can be called after the instance is saved manually at a later time. Return the model instance.
300 def to_data_dict(self): 301 self.data_dict = self._project_dict() \ 302 | self._department_dict() \ 303 | self._general_costs_dict() \ 304 | self._department_costs_dict() \ 305 | self._project_funded_staff_dict() \ 306 | self._salary_costs_annotated_dict() 307 308 self.data_dict['department_costs'].update( 309 { 310 'salary_costs': self.department_costs.equivalents_per_hour 311 * (self.data_dict['project_funded_staff']['hours_sum'] 312 + self.data_dict['salary_costs_annotated']['hours_sum']), 313 } 314 ) 315 self.data_dict['general_costs'].update( 316 { 317 'total': (self.data_dict['project_funded_staff']['hours_sum'] 318 + self.data_dict['salary_costs_annotated']['hours_sum']) 319 * self.data_dict['general_costs']['costs'], 320 } 321 ) 322 323 return self.data_dict
Inherited Members
- django.forms.models.BaseModelForm
- validate_unique
- django.forms.forms.BaseForm
- order_fields
- errors
- is_valid
- add_prefix
- add_initial_prefix
- get_context
- non_field_errors
- add_error
- has_error
- full_clean
- has_changed
- changed_data
- is_multipart
- visible_fields
- get_initial_for_field
- django.forms.utils.RenderableFormMixin
- as_p
- as_table
- as_ul
- as_div
- django.forms.utils.RenderableMixin
- render
329class CostumJSONEncoder(DjangoJSONEncoder): 330 def default(self, o): 331 if isinstance(o, Decimal): 332 return str(round(o, 2)).replace(".", ",") 333 else: 334 return super().default(o)
JSONEncoder subclass that knows how to encode date/time, decimal types, and UUIDs.
330 def default(self, o): 331 if isinstance(o, Decimal): 332 return str(round(o, 2)).replace(".", ",") 333 else: 334 return super().default(o)
Implement this method in a subclass such that it returns
a serializable object for o
, or calls the base implementation
(to raise a TypeError
).
For example, to support arbitrary iterators, you could implement default like this::
def default(self, o):
try:
iterable = iter(o)
except TypeError:
pass
else:
return list(iterable)
# Let the base class default method raise the TypeError
return JSONEncoder.default(self, o)
Inherited Members
- json.encoder.JSONEncoder
- JSONEncoder
- encode
- iterencode
337class ReceiptTemplateSelectForm(forms.Form): 338 def __init__(self, *args, **kwargs): 339 super().__init__(*args, **kwargs) 340 queryset = models.ReceiptTemplate.objects.all().order_by('-start') 341 self.fields["receipt_template"] = forms.ModelChoiceField( 342 queryset, 343 empty_label=None, 344 label=_('Receipt Template') 345 )
A collection of Fields, plus their associated data.
Inherited Members
- django.forms.forms.BaseForm
- order_fields
- errors
- is_valid
- add_prefix
- add_initial_prefix
- get_context
- non_field_errors
- add_error
- has_error
- full_clean
- clean
- has_changed
- changed_data
- is_multipart
- visible_fields
- get_initial_for_field
- django.forms.utils.RenderableFormMixin
- as_p
- as_table
- as_ul
- as_div
- django.forms.utils.RenderableMixin
- render