vkk.workhours.accounting.projects.project.export.receipts.forms
A collection of forms used in this module.
1""" 2A collection of forms used in this module. 3""" 4 5from django import forms 6from decimal import Decimal 7from django.db.models import Sum, F 8from django.core.exceptions import ValidationError 9from django.core.serializers.json import DjangoJSONEncoder 10from django.utils.translation import gettext_lazy as _ 11from vkk.workhours import models 12from vkk.generic.forms import CustomDateInput 13 14 15class ReceiptForm(forms.ModelForm): 16 """ 17 A `Form` sublcass for generating a receipt. This mimics the papaer receipts 18 used previously. 19 """ 20 class Meta: 21 model = models.Receipt 22 fields = ['start', 'end', 'receipt_number', 'buper'] 23 widgets = { 24 'start': CustomDateInput, 25 'end': CustomDateInput, 26 } 27 28 class Media: 29 js = ('scripts/receipts.js',) 30 31 def __init__(self, *args, project=None, **kwargs): 32 """ 33 Initializes and returns a new object of this class. A `Project` instance must be provided. 34 """ 35 super().__init__(*args, **kwargs) 36 self.project = project 37 self.department = self.project.department 38 self.general_costs = None 39 self.department_costs = None 40 self.project_funded_staff_date = None 41 self.project_funded_staff = None 42 self.salary_level_date = None 43 self.salary_costs = None 44 self.salary_costs_annotated1 = None 45 self.salary_costs_annotated2 = None 46 self.data_dict = None 47 48 def set_and_clean_general_costs(self, start, end): 49 """ 50 Sets and cleans data associated to `GeneralCosts`. 51 """ 52 if models.GeneralCosts.objects.filter(start__gt=start, start__lte=end,).exists(): 53 raise ValidationError( 54 _('General cost records are ambiguous.'), 55 code='ambiguous_general_costs' 56 ) 57 try: 58 self.general_costs = models.GeneralCosts.objects.filter( 59 start__lte=start 60 ).latest('start') 61 except models.GeneralCosts.DoesNotExist: 62 raise ValidationError( 63 _('No valid general cost record found'), 64 code='no_general_costs' 65 ) 66 67 def set_and_clean_department_costs(self, start, end): 68 """ 69 Sets and cleans data associated to `DepartmentCosts`. 70 """ 71 if models.DepartmentCosts.objects.filter( 72 department=self.department, 73 start__date__gt=start, 74 start__date__lte=end, 75 ).exists(): 76 raise ValidationError( 77 _('Department cost records are ambiguous.'), 78 code='ambiguous_department_costs' 79 ) 80 try: 81 self.department_costs = models.DepartmentCosts.objects.filter( 82 department=self.department, 83 start__date__lte=start, 84 ).latest('start__date') 85 except models.DepartmentCosts.DoesNotExist: 86 raise ValidationError( 87 _('No valid department cost record found'), 88 code='no_department_costs' 89 ) 90 91 def set_and_clean_project_funded_staff(self, start, end): 92 """ 93 Sets and cleans data associated to `ProjectFundedStaff`. 94 """ 95 if models.ProjectFundedStaffDate.objects.filter( 96 project=self.project, 97 date__gt=start, 98 date__lte=end, 99 ).exists(): 100 raise ValidationError( 101 _('Project funded staff records are ambiguous.'), 102 code='ambiguous_staff_costs' 103 ) 104 try: 105 self.project_funded_staff_date = models.ProjectFundedStaffDate.objects.filter( 106 project=self.project, 107 date__lte=start, 108 ).latest('date') 109 except models.ProjectFundedStaffDate.DoesNotExist: 110 # This is valid behaviour, no exception needed 111 pass 112 if self.project_funded_staff_date is not None: 113 self.project_funded_staff = models.ProjectFundedStaff.objects.filter( 114 start=self.project_funded_staff_date 115 ) 116 117 def set_and_clean_salary_level(self, start, end): 118 """ 119 Sets and cleans data associated to `SalaryLevelCosts`. 120 """ 121 if models.SalaryLevelDate.objects.filter( 122 date__gt=start, 123 date__lte=end, 124 ).exists(): 125 raise ValidationError( 126 _('Salary Level records are ambiguous.'), 127 code='ambiguous_salary_costs' 128 ) 129 try: 130 self.salary_level_date = models.SalaryLevelDate.objects.filter( 131 date__lte=start 132 ).latest('date') 133 except: 134 raise ValidationError( 135 _('No valid salary level cost records found'), 136 code='no_salary_costs' 137 ) 138 if self.salary_level_date is not None: 139 self.salary_costs = models.SalaryLevelCosts.objects.filter( 140 start=self.salary_level_date 141 ) 142 143 def check_peroid_overlap(self, start, end): 144 """ 145 Checks whether the given start and end dates overlap with only one period. 146 """ 147 # check for matching period 148 if not models.Period.objects.filter(start=start, end=end).exists(): 149 raise ValidationError( 150 _('End and Start do not match up with given periods'), 151 code='period_ambiguous' 152 ) 153 154 def check_closed_periods(self, start, end): 155 """ 156 Checks whether the period has been closed. 157 """ 158 assignments_not_closed = models.ProjectAssignment.objects.filter( 159 project=self.project 160 ).exclude( 161 periodclosure__period__start=start, 162 periodclosure__period__end=end, 163 periodclosure__is_closed_manager=True, 164 periodclosure__is_closed_contributor=True, 165 ) 166 if assignments_not_closed.exists(): 167 raise ValidationError( 168 _('Some contributors or project managers did not close their work hour inputs'), 169 code='periods_not_closed' 170 ) 171 172 def set_and_clean_workhours(self, start, end): 173 """ 174 Sets and cleans work hours. (Summed) 175 """ 176 if self.salary_costs is not None: 177 # Specify constraints for aggregations 178 agg1 = self.salary_costs.filter( 179 salary_level__projectassignment__project=self.project, 180 salary_level__projectassignment__workhours__day__gte=start, 181 salary_level__projectassignment__workhours__day__lte=end, 182 ) 183 # Annotate work hours 184 agg1 = agg1.annotate( 185 workhours=Sum( 186 'salary_level__projectassignment__workhours__hours') 187 ) 188 agg1 = agg1.annotate( 189 costs=F('brutto_per_hour') * 190 Sum('salary_level__projectassignment__workhours__hours') 191 ) 192 193 self.salary_costs_annotated1 = agg1 194 195 agg2 = self.salary_costs.filter( 196 salary_level__projectassignment__project=self.project, 197 salary_level__projectassignment__workhourscorrection__period__start=start, 198 salary_level__projectassignment__workhourscorrection__period__end=end, 199 ) 200 agg2 = agg2.annotate( 201 workhours_correction=Sum( 202 'salary_level__projectassignment__workhourscorrection__ammount' 203 ) 204 ) 205 agg2 = agg2.annotate( 206 costs=F('brutto_per_hour') * 207 Sum('salary_level__projectassignment__workhourscorrection__ammount') 208 ) 209 210 self.salary_costs_annotated2 = agg2 211 212 def clean(self): 213 """ 214 Cleans the associated data of this object and returns it as a dictionary. 215 """ 216 start = self.cleaned_data.get('start') 217 end = self.cleaned_data.get('end') 218 if start is not None and end is not None: 219 self.set_and_clean_general_costs(start, end) 220 self.set_and_clean_department_costs(start, end) 221 self.set_and_clean_project_funded_staff(start, end) 222 self.set_and_clean_salary_level(start, end) 223 self.check_peroid_overlap(start, end) 224 self.check_closed_periods(start, end) 225 self.set_and_clean_workhours(start, end) 226 return self.cleaned_data 227 228 def save(self, commit=True): 229 """ 230 Tries to save the data associated with this form as a receipt. 231 """ 232 self.instance.project = self.project 233 self.instance.data = self.to_json() 234 return super().save(commit) 235 236 def _project_dict(self): 237 return { 238 'project': { 239 'invoice_number': self.project.invoice_number, 240 'name': self.project.name, 241 'contractor': self.project.contractor, 242 'start': self.project.start, 243 'end': self.project.end, 244 } 245 } 246 247 def _department_dict(self): 248 return { 249 'department': { 250 'name': self.department.name, 251 'accounting_entry': self.department.accounting_entry, 252 'invoice_number': self.department.invoice_number, 253 } 254 } 255 256 def _general_costs_dict(self): 257 return { 258 'general_costs': { 259 'start': self.general_costs.start, 260 'costs': self.general_costs.costs, 261 } 262 } 263 264 def _department_costs_dict(self): 265 return { 266 'department_costs': { 267 'start': self.department_costs.start.date, 268 'equivalents_per_hour': self.department_costs.equivalents_per_hour, 269 } 270 } 271 272 def _project_funded_staff_dict(self): 273 return { 274 'project_funded_staff': { 275 'start': self.project_funded_staff_date.date if 276 self.project_funded_staff_date is not None else None, 277 'hours_by_salary_level': { 278 entry.salary_level.salary_code: { 279 'hours': entry.hours, 280 'brutto_per_hour': self.salary_costs.get( 281 salary_level=entry.salary_level 282 ).brutto_per_hour, 283 } for entry in self.project_funded_staff or [] 284 }, 285 'hours_sum': sum([ 286 entry.hours for entry in self.project_funded_staff or [] 287 ]), 288 } 289 } 290 291 def _salary_costs_annotated_dict(self): 292 summed_costs = [] 293 294 for entry in self.salary_costs: 295 workhours, costs, code, brutto = None, None, None, None 296 annotated1 = self.salary_costs_annotated1.filter(id=entry.id) 297 if annotated1.exists(): 298 code = annotated1[0].salary_level.salary_code 299 brutto = annotated1[0].brutto_per_hour 300 workhours = annotated1[0].workhours 301 costs = annotated1[0].costs 302 303 annotated2 = self.salary_costs_annotated2.filter(id=entry.id) 304 if annotated2.exists(): 305 code = annotated2[0].salary_level.salary_code 306 brutto = annotated2[0].brutto_per_hour 307 if workhours is None: 308 workhours = 0 309 if costs is None: 310 costs = 0 311 workhours += annotated2[0].workhours_correction 312 costs += annotated2[0].costs 313 314 if workhours is not None and costs is not None \ 315 and code is not None and brutto is not None: 316 summed_costs.append((workhours, costs, code, brutto)) 317 318 return { 319 'salary_costs_annotated': { 320 'start': self.salary_level_date.date, 321 'salary_levels': { 322 entry[2]: { 323 'brutto_per_hour': entry[3], 324 'hours': entry[0], 325 'costs': entry[1], 326 } for entry in summed_costs 327 }, 328 'hours_sum': sum([ 329 entry[0] for entry in summed_costs 330 ]), 331 'costs_sum': sum([ 332 entry[1] for entry in summed_costs 333 ]), 334 } 335 } 336 337 def to_data_dict(self): 338 """ 339 Returns the data associated with this form as a dictionary. 340 """ 341 self.data_dict = self._project_dict() \ 342 | self._department_dict() \ 343 | self._general_costs_dict() \ 344 | self._department_costs_dict() \ 345 | self._project_funded_staff_dict() \ 346 | self._salary_costs_annotated_dict() 347 348 self.data_dict['department_costs'].update( 349 { 350 'salary_costs': self.department_costs.equivalents_per_hour 351 * (self.data_dict['project_funded_staff']['hours_sum'] 352 + self.data_dict['salary_costs_annotated']['hours_sum']), 353 } 354 ) 355 self.data_dict['general_costs'].update( 356 { 357 'total': (self.data_dict['project_funded_staff']['hours_sum'] 358 + self.data_dict['salary_costs_annotated']['hours_sum']) 359 * self.data_dict['general_costs']['costs'], 360 } 361 ) 362 363 return self.data_dict 364 365 def to_json(self): 366 """ 367 Encodes and returns the data associated with this form in JSON. 368 """ 369 return CostumJSONEncoder().encode(self.to_data_dict()) 370 371 372class CostumJSONEncoder(DjangoJSONEncoder): 373 """ 374 A costum JSON encoder which rounds numbers after to decimal places and uses 375 commas instead of points. 376 """ 377 def default(self, o): 378 if isinstance(o, Decimal): 379 return str(round(o, 2)).replace(".", ",") 380 else: 381 return super().default(o) 382 383 384class ReceiptTemplateSelectForm(forms.Form): 385 """ 386 A `Form` subclass for selecting a specific receipt template. 387 """ 388 def __init__(self, *args, **kwargs): 389 super().__init__(*args, **kwargs) 390 queryset = models.ReceiptTemplate.objects.all().order_by('-start') 391 self.fields["receipt_template"] = forms.ModelChoiceField( 392 queryset, 393 empty_label=None, 394 label=_('Receipt Template') 395 )
16class ReceiptForm(forms.ModelForm): 17 """ 18 A `Form` sublcass for generating a receipt. This mimics the papaer receipts 19 used previously. 20 """ 21 class Meta: 22 model = models.Receipt 23 fields = ['start', 'end', 'receipt_number', 'buper'] 24 widgets = { 25 'start': CustomDateInput, 26 'end': CustomDateInput, 27 } 28 29 class Media: 30 js = ('scripts/receipts.js',) 31 32 def __init__(self, *args, project=None, **kwargs): 33 """ 34 Initializes and returns a new object of this class. A `Project` instance must be provided. 35 """ 36 super().__init__(*args, **kwargs) 37 self.project = project 38 self.department = self.project.department 39 self.general_costs = None 40 self.department_costs = None 41 self.project_funded_staff_date = None 42 self.project_funded_staff = None 43 self.salary_level_date = None 44 self.salary_costs = None 45 self.salary_costs_annotated1 = None 46 self.salary_costs_annotated2 = None 47 self.data_dict = None 48 49 def set_and_clean_general_costs(self, start, end): 50 """ 51 Sets and cleans data associated to `GeneralCosts`. 52 """ 53 if models.GeneralCosts.objects.filter(start__gt=start, start__lte=end,).exists(): 54 raise ValidationError( 55 _('General cost records are ambiguous.'), 56 code='ambiguous_general_costs' 57 ) 58 try: 59 self.general_costs = models.GeneralCosts.objects.filter( 60 start__lte=start 61 ).latest('start') 62 except models.GeneralCosts.DoesNotExist: 63 raise ValidationError( 64 _('No valid general cost record found'), 65 code='no_general_costs' 66 ) 67 68 def set_and_clean_department_costs(self, start, end): 69 """ 70 Sets and cleans data associated to `DepartmentCosts`. 71 """ 72 if models.DepartmentCosts.objects.filter( 73 department=self.department, 74 start__date__gt=start, 75 start__date__lte=end, 76 ).exists(): 77 raise ValidationError( 78 _('Department cost records are ambiguous.'), 79 code='ambiguous_department_costs' 80 ) 81 try: 82 self.department_costs = models.DepartmentCosts.objects.filter( 83 department=self.department, 84 start__date__lte=start, 85 ).latest('start__date') 86 except models.DepartmentCosts.DoesNotExist: 87 raise ValidationError( 88 _('No valid department cost record found'), 89 code='no_department_costs' 90 ) 91 92 def set_and_clean_project_funded_staff(self, start, end): 93 """ 94 Sets and cleans data associated to `ProjectFundedStaff`. 95 """ 96 if models.ProjectFundedStaffDate.objects.filter( 97 project=self.project, 98 date__gt=start, 99 date__lte=end, 100 ).exists(): 101 raise ValidationError( 102 _('Project funded staff records are ambiguous.'), 103 code='ambiguous_staff_costs' 104 ) 105 try: 106 self.project_funded_staff_date = models.ProjectFundedStaffDate.objects.filter( 107 project=self.project, 108 date__lte=start, 109 ).latest('date') 110 except models.ProjectFundedStaffDate.DoesNotExist: 111 # This is valid behaviour, no exception needed 112 pass 113 if self.project_funded_staff_date is not None: 114 self.project_funded_staff = models.ProjectFundedStaff.objects.filter( 115 start=self.project_funded_staff_date 116 ) 117 118 def set_and_clean_salary_level(self, start, end): 119 """ 120 Sets and cleans data associated to `SalaryLevelCosts`. 121 """ 122 if models.SalaryLevelDate.objects.filter( 123 date__gt=start, 124 date__lte=end, 125 ).exists(): 126 raise ValidationError( 127 _('Salary Level records are ambiguous.'), 128 code='ambiguous_salary_costs' 129 ) 130 try: 131 self.salary_level_date = models.SalaryLevelDate.objects.filter( 132 date__lte=start 133 ).latest('date') 134 except: 135 raise ValidationError( 136 _('No valid salary level cost records found'), 137 code='no_salary_costs' 138 ) 139 if self.salary_level_date is not None: 140 self.salary_costs = models.SalaryLevelCosts.objects.filter( 141 start=self.salary_level_date 142 ) 143 144 def check_peroid_overlap(self, start, end): 145 """ 146 Checks whether the given start and end dates overlap with only one period. 147 """ 148 # check for matching period 149 if not models.Period.objects.filter(start=start, end=end).exists(): 150 raise ValidationError( 151 _('End and Start do not match up with given periods'), 152 code='period_ambiguous' 153 ) 154 155 def check_closed_periods(self, start, end): 156 """ 157 Checks whether the period has been closed. 158 """ 159 assignments_not_closed = models.ProjectAssignment.objects.filter( 160 project=self.project 161 ).exclude( 162 periodclosure__period__start=start, 163 periodclosure__period__end=end, 164 periodclosure__is_closed_manager=True, 165 periodclosure__is_closed_contributor=True, 166 ) 167 if assignments_not_closed.exists(): 168 raise ValidationError( 169 _('Some contributors or project managers did not close their work hour inputs'), 170 code='periods_not_closed' 171 ) 172 173 def set_and_clean_workhours(self, start, end): 174 """ 175 Sets and cleans work hours. (Summed) 176 """ 177 if self.salary_costs is not None: 178 # Specify constraints for aggregations 179 agg1 = self.salary_costs.filter( 180 salary_level__projectassignment__project=self.project, 181 salary_level__projectassignment__workhours__day__gte=start, 182 salary_level__projectassignment__workhours__day__lte=end, 183 ) 184 # Annotate work hours 185 agg1 = agg1.annotate( 186 workhours=Sum( 187 'salary_level__projectassignment__workhours__hours') 188 ) 189 agg1 = agg1.annotate( 190 costs=F('brutto_per_hour') * 191 Sum('salary_level__projectassignment__workhours__hours') 192 ) 193 194 self.salary_costs_annotated1 = agg1 195 196 agg2 = self.salary_costs.filter( 197 salary_level__projectassignment__project=self.project, 198 salary_level__projectassignment__workhourscorrection__period__start=start, 199 salary_level__projectassignment__workhourscorrection__period__end=end, 200 ) 201 agg2 = agg2.annotate( 202 workhours_correction=Sum( 203 'salary_level__projectassignment__workhourscorrection__ammount' 204 ) 205 ) 206 agg2 = agg2.annotate( 207 costs=F('brutto_per_hour') * 208 Sum('salary_level__projectassignment__workhourscorrection__ammount') 209 ) 210 211 self.salary_costs_annotated2 = agg2 212 213 def clean(self): 214 """ 215 Cleans the associated data of this object and returns it as a dictionary. 216 """ 217 start = self.cleaned_data.get('start') 218 end = self.cleaned_data.get('end') 219 if start is not None and end is not None: 220 self.set_and_clean_general_costs(start, end) 221 self.set_and_clean_department_costs(start, end) 222 self.set_and_clean_project_funded_staff(start, end) 223 self.set_and_clean_salary_level(start, end) 224 self.check_peroid_overlap(start, end) 225 self.check_closed_periods(start, end) 226 self.set_and_clean_workhours(start, end) 227 return self.cleaned_data 228 229 def save(self, commit=True): 230 """ 231 Tries to save the data associated with this form as a receipt. 232 """ 233 self.instance.project = self.project 234 self.instance.data = self.to_json() 235 return super().save(commit) 236 237 def _project_dict(self): 238 return { 239 'project': { 240 'invoice_number': self.project.invoice_number, 241 'name': self.project.name, 242 'contractor': self.project.contractor, 243 'start': self.project.start, 244 'end': self.project.end, 245 } 246 } 247 248 def _department_dict(self): 249 return { 250 'department': { 251 'name': self.department.name, 252 'accounting_entry': self.department.accounting_entry, 253 'invoice_number': self.department.invoice_number, 254 } 255 } 256 257 def _general_costs_dict(self): 258 return { 259 'general_costs': { 260 'start': self.general_costs.start, 261 'costs': self.general_costs.costs, 262 } 263 } 264 265 def _department_costs_dict(self): 266 return { 267 'department_costs': { 268 'start': self.department_costs.start.date, 269 'equivalents_per_hour': self.department_costs.equivalents_per_hour, 270 } 271 } 272 273 def _project_funded_staff_dict(self): 274 return { 275 'project_funded_staff': { 276 'start': self.project_funded_staff_date.date if 277 self.project_funded_staff_date is not None else None, 278 'hours_by_salary_level': { 279 entry.salary_level.salary_code: { 280 'hours': entry.hours, 281 'brutto_per_hour': self.salary_costs.get( 282 salary_level=entry.salary_level 283 ).brutto_per_hour, 284 } for entry in self.project_funded_staff or [] 285 }, 286 'hours_sum': sum([ 287 entry.hours for entry in self.project_funded_staff or [] 288 ]), 289 } 290 } 291 292 def _salary_costs_annotated_dict(self): 293 summed_costs = [] 294 295 for entry in self.salary_costs: 296 workhours, costs, code, brutto = None, None, None, None 297 annotated1 = self.salary_costs_annotated1.filter(id=entry.id) 298 if annotated1.exists(): 299 code = annotated1[0].salary_level.salary_code 300 brutto = annotated1[0].brutto_per_hour 301 workhours = annotated1[0].workhours 302 costs = annotated1[0].costs 303 304 annotated2 = self.salary_costs_annotated2.filter(id=entry.id) 305 if annotated2.exists(): 306 code = annotated2[0].salary_level.salary_code 307 brutto = annotated2[0].brutto_per_hour 308 if workhours is None: 309 workhours = 0 310 if costs is None: 311 costs = 0 312 workhours += annotated2[0].workhours_correction 313 costs += annotated2[0].costs 314 315 if workhours is not None and costs is not None \ 316 and code is not None and brutto is not None: 317 summed_costs.append((workhours, costs, code, brutto)) 318 319 return { 320 'salary_costs_annotated': { 321 'start': self.salary_level_date.date, 322 'salary_levels': { 323 entry[2]: { 324 'brutto_per_hour': entry[3], 325 'hours': entry[0], 326 'costs': entry[1], 327 } for entry in summed_costs 328 }, 329 'hours_sum': sum([ 330 entry[0] for entry in summed_costs 331 ]), 332 'costs_sum': sum([ 333 entry[1] for entry in summed_costs 334 ]), 335 } 336 } 337 338 def to_data_dict(self): 339 """ 340 Returns the data associated with this form as a dictionary. 341 """ 342 self.data_dict = self._project_dict() \ 343 | self._department_dict() \ 344 | self._general_costs_dict() \ 345 | self._department_costs_dict() \ 346 | self._project_funded_staff_dict() \ 347 | self._salary_costs_annotated_dict() 348 349 self.data_dict['department_costs'].update( 350 { 351 'salary_costs': self.department_costs.equivalents_per_hour 352 * (self.data_dict['project_funded_staff']['hours_sum'] 353 + self.data_dict['salary_costs_annotated']['hours_sum']), 354 } 355 ) 356 self.data_dict['general_costs'].update( 357 { 358 'total': (self.data_dict['project_funded_staff']['hours_sum'] 359 + self.data_dict['salary_costs_annotated']['hours_sum']) 360 * self.data_dict['general_costs']['costs'], 361 } 362 ) 363 364 return self.data_dict 365 366 def to_json(self): 367 """ 368 Encodes and returns the data associated with this form in JSON. 369 """ 370 return CostumJSONEncoder().encode(self.to_data_dict())
A Form
sublcass for generating a receipt. This mimics the papaer receipts
used previously.
32 def __init__(self, *args, project=None, **kwargs): 33 """ 34 Initializes and returns a new object of this class. A `Project` instance must be provided. 35 """ 36 super().__init__(*args, **kwargs) 37 self.project = project 38 self.department = self.project.department 39 self.general_costs = None 40 self.department_costs = None 41 self.project_funded_staff_date = None 42 self.project_funded_staff = None 43 self.salary_level_date = None 44 self.salary_costs = None 45 self.salary_costs_annotated1 = None 46 self.salary_costs_annotated2 = None 47 self.data_dict = None
Initializes and returns a new object of this class. A Project
instance must be provided.
49 def set_and_clean_general_costs(self, start, end): 50 """ 51 Sets and cleans data associated to `GeneralCosts`. 52 """ 53 if models.GeneralCosts.objects.filter(start__gt=start, start__lte=end,).exists(): 54 raise ValidationError( 55 _('General cost records are ambiguous.'), 56 code='ambiguous_general_costs' 57 ) 58 try: 59 self.general_costs = models.GeneralCosts.objects.filter( 60 start__lte=start 61 ).latest('start') 62 except models.GeneralCosts.DoesNotExist: 63 raise ValidationError( 64 _('No valid general cost record found'), 65 code='no_general_costs' 66 )
Sets and cleans data associated to GeneralCosts
.
68 def set_and_clean_department_costs(self, start, end): 69 """ 70 Sets and cleans data associated to `DepartmentCosts`. 71 """ 72 if models.DepartmentCosts.objects.filter( 73 department=self.department, 74 start__date__gt=start, 75 start__date__lte=end, 76 ).exists(): 77 raise ValidationError( 78 _('Department cost records are ambiguous.'), 79 code='ambiguous_department_costs' 80 ) 81 try: 82 self.department_costs = models.DepartmentCosts.objects.filter( 83 department=self.department, 84 start__date__lte=start, 85 ).latest('start__date') 86 except models.DepartmentCosts.DoesNotExist: 87 raise ValidationError( 88 _('No valid department cost record found'), 89 code='no_department_costs' 90 )
Sets and cleans data associated to DepartmentCosts
.
92 def set_and_clean_project_funded_staff(self, start, end): 93 """ 94 Sets and cleans data associated to `ProjectFundedStaff`. 95 """ 96 if models.ProjectFundedStaffDate.objects.filter( 97 project=self.project, 98 date__gt=start, 99 date__lte=end, 100 ).exists(): 101 raise ValidationError( 102 _('Project funded staff records are ambiguous.'), 103 code='ambiguous_staff_costs' 104 ) 105 try: 106 self.project_funded_staff_date = models.ProjectFundedStaffDate.objects.filter( 107 project=self.project, 108 date__lte=start, 109 ).latest('date') 110 except models.ProjectFundedStaffDate.DoesNotExist: 111 # This is valid behaviour, no exception needed 112 pass 113 if self.project_funded_staff_date is not None: 114 self.project_funded_staff = models.ProjectFundedStaff.objects.filter( 115 start=self.project_funded_staff_date 116 )
Sets and cleans data associated to ProjectFundedStaff
.
118 def set_and_clean_salary_level(self, start, end): 119 """ 120 Sets and cleans data associated to `SalaryLevelCosts`. 121 """ 122 if models.SalaryLevelDate.objects.filter( 123 date__gt=start, 124 date__lte=end, 125 ).exists(): 126 raise ValidationError( 127 _('Salary Level records are ambiguous.'), 128 code='ambiguous_salary_costs' 129 ) 130 try: 131 self.salary_level_date = models.SalaryLevelDate.objects.filter( 132 date__lte=start 133 ).latest('date') 134 except: 135 raise ValidationError( 136 _('No valid salary level cost records found'), 137 code='no_salary_costs' 138 ) 139 if self.salary_level_date is not None: 140 self.salary_costs = models.SalaryLevelCosts.objects.filter( 141 start=self.salary_level_date 142 )
Sets and cleans data associated to SalaryLevelCosts
.
144 def check_peroid_overlap(self, start, end): 145 """ 146 Checks whether the given start and end dates overlap with only one period. 147 """ 148 # check for matching period 149 if not models.Period.objects.filter(start=start, end=end).exists(): 150 raise ValidationError( 151 _('End and Start do not match up with given periods'), 152 code='period_ambiguous' 153 )
Checks whether the given start and end dates overlap with only one period.
155 def check_closed_periods(self, start, end): 156 """ 157 Checks whether the period has been closed. 158 """ 159 assignments_not_closed = models.ProjectAssignment.objects.filter( 160 project=self.project 161 ).exclude( 162 periodclosure__period__start=start, 163 periodclosure__period__end=end, 164 periodclosure__is_closed_manager=True, 165 periodclosure__is_closed_contributor=True, 166 ) 167 if assignments_not_closed.exists(): 168 raise ValidationError( 169 _('Some contributors or project managers did not close their work hour inputs'), 170 code='periods_not_closed' 171 )
Checks whether the period has been closed.
173 def set_and_clean_workhours(self, start, end): 174 """ 175 Sets and cleans work hours. (Summed) 176 """ 177 if self.salary_costs is not None: 178 # Specify constraints for aggregations 179 agg1 = self.salary_costs.filter( 180 salary_level__projectassignment__project=self.project, 181 salary_level__projectassignment__workhours__day__gte=start, 182 salary_level__projectassignment__workhours__day__lte=end, 183 ) 184 # Annotate work hours 185 agg1 = agg1.annotate( 186 workhours=Sum( 187 'salary_level__projectassignment__workhours__hours') 188 ) 189 agg1 = agg1.annotate( 190 costs=F('brutto_per_hour') * 191 Sum('salary_level__projectassignment__workhours__hours') 192 ) 193 194 self.salary_costs_annotated1 = agg1 195 196 agg2 = self.salary_costs.filter( 197 salary_level__projectassignment__project=self.project, 198 salary_level__projectassignment__workhourscorrection__period__start=start, 199 salary_level__projectassignment__workhourscorrection__period__end=end, 200 ) 201 agg2 = agg2.annotate( 202 workhours_correction=Sum( 203 'salary_level__projectassignment__workhourscorrection__ammount' 204 ) 205 ) 206 agg2 = agg2.annotate( 207 costs=F('brutto_per_hour') * 208 Sum('salary_level__projectassignment__workhourscorrection__ammount') 209 ) 210 211 self.salary_costs_annotated2 = agg2
Sets and cleans work hours. (Summed)
213 def clean(self): 214 """ 215 Cleans the associated data of this object and returns it as a dictionary. 216 """ 217 start = self.cleaned_data.get('start') 218 end = self.cleaned_data.get('end') 219 if start is not None and end is not None: 220 self.set_and_clean_general_costs(start, end) 221 self.set_and_clean_department_costs(start, end) 222 self.set_and_clean_project_funded_staff(start, end) 223 self.set_and_clean_salary_level(start, end) 224 self.check_peroid_overlap(start, end) 225 self.check_closed_periods(start, end) 226 self.set_and_clean_workhours(start, end) 227 return self.cleaned_data
Cleans the associated data of this object and returns it as a dictionary.
229 def save(self, commit=True): 230 """ 231 Tries to save the data associated with this form as a receipt. 232 """ 233 self.instance.project = self.project 234 self.instance.data = self.to_json() 235 return super().save(commit)
Tries to save the data associated with this form as a receipt.
338 def to_data_dict(self): 339 """ 340 Returns the data associated with this form as a dictionary. 341 """ 342 self.data_dict = self._project_dict() \ 343 | self._department_dict() \ 344 | self._general_costs_dict() \ 345 | self._department_costs_dict() \ 346 | self._project_funded_staff_dict() \ 347 | self._salary_costs_annotated_dict() 348 349 self.data_dict['department_costs'].update( 350 { 351 'salary_costs': self.department_costs.equivalents_per_hour 352 * (self.data_dict['project_funded_staff']['hours_sum'] 353 + self.data_dict['salary_costs_annotated']['hours_sum']), 354 } 355 ) 356 self.data_dict['general_costs'].update( 357 { 358 'total': (self.data_dict['project_funded_staff']['hours_sum'] 359 + self.data_dict['salary_costs_annotated']['hours_sum']) 360 * self.data_dict['general_costs']['costs'], 361 } 362 ) 363 364 return self.data_dict
Returns the data associated with this form as a dictionary.
366 def to_json(self): 367 """ 368 Encodes and returns the data associated with this form in JSON. 369 """ 370 return CostumJSONEncoder().encode(self.to_data_dict())
Encodes and returns the data associated with this form in JSON.
Inherited Members
- django.forms.models.BaseModelForm
- validate_unique
- django.forms.forms.BaseForm
- default_renderer
- field_order
- prefix
- use_required_attribute
- template_name_div
- template_name_p
- template_name_table
- template_name_ul
- template_name_label
- is_bound
- data
- files
- auto_id
- initial
- error_class
- label_suffix
- empty_permitted
- fields
- renderer
- order_fields
- errors
- is_valid
- add_prefix
- add_initial_prefix
- template_name
- 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
21 class Meta: 22 model = models.Receipt 23 fields = ['start', 'end', 'receipt_number', 'buper'] 24 widgets = { 25 'start': CustomDateInput, 26 'end': CustomDateInput, 27 }
373class CostumJSONEncoder(DjangoJSONEncoder): 374 """ 375 A costum JSON encoder which rounds numbers after to decimal places and uses 376 commas instead of points. 377 """ 378 def default(self, o): 379 if isinstance(o, Decimal): 380 return str(round(o, 2)).replace(".", ",") 381 else: 382 return super().default(o)
A costum JSON encoder which rounds numbers after to decimal places and uses commas instead of points.
378 def default(self, o): 379 if isinstance(o, Decimal): 380 return str(round(o, 2)).replace(".", ",") 381 else: 382 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
- item_separator
- key_separator
- skipkeys
- ensure_ascii
- check_circular
- allow_nan
- sort_keys
- indent
- encode
- iterencode
385class ReceiptTemplateSelectForm(forms.Form): 386 """ 387 A `Form` subclass for selecting a specific receipt template. 388 """ 389 def __init__(self, *args, **kwargs): 390 super().__init__(*args, **kwargs) 391 queryset = models.ReceiptTemplate.objects.all().order_by('-start') 392 self.fields["receipt_template"] = forms.ModelChoiceField( 393 queryset, 394 empty_label=None, 395 label=_('Receipt Template') 396 )
A Form
subclass for selecting a specific receipt template.
Inherited Members
- django.forms.forms.BaseForm
- default_renderer
- field_order
- prefix
- use_required_attribute
- template_name_div
- template_name_p
- template_name_table
- template_name_ul
- template_name_label
- is_bound
- data
- files
- auto_id
- initial
- error_class
- label_suffix
- empty_permitted
- fields
- renderer
- order_fields
- errors
- is_valid
- add_prefix
- add_initial_prefix
- template_name
- 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