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        )
class ReceiptForm(django.forms.models.ModelForm):
 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.

ReceiptForm(*args, project=None, **kwargs)
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.

project
department
general_costs
department_costs
project_funded_staff_date
project_funded_staff
salary_level_date
salary_costs
salary_costs_annotated1
salary_costs_annotated2
data_dict
def set_and_clean_general_costs(self, start, end):
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.

def set_and_clean_department_costs(self, start, end):
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.

def set_and_clean_project_funded_staff(self, start, end):
 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.

def set_and_clean_salary_level(self, start, end):
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.

def check_peroid_overlap(self, start, end):
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.

def check_closed_periods(self, start, end):
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.

def set_and_clean_workhours(self, start, end):
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)

def clean(self):
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.

def save(self, commit=True):
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.

def to_data_dict(self):
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.

def to_json(self):
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.

media

Return all media required to render the widgets on this form.

declared_fields = {}
base_fields = {'start': <django.forms.fields.DateField object>, 'end': <django.forms.fields.DateField object>, 'receipt_number': <django.forms.fields.IntegerField object>, 'buper': <django.forms.fields.IntegerField object>}
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
hidden_fields
visible_fields
get_initial_for_field
django.forms.utils.RenderableFormMixin
as_p
as_table
as_ul
as_div
django.forms.utils.RenderableMixin
render
class ReceiptForm.Meta:
21    class Meta:
22        model = models.Receipt
23        fields = ['start', 'end', 'receipt_number', 'buper']
24        widgets = {
25            'start': CustomDateInput,
26            'end': CustomDateInput,
27        }
model = <class 'vkk.workhours.models.Receipt'>
fields = ['start', 'end', 'receipt_number', 'buper']
widgets = {'start': <class 'vkk.generic.forms.CustomDateInput'>, 'end': <class 'vkk.generic.forms.CustomDateInput'>}
class ReceiptForm.Media:
29    class Media:
30        js = ('scripts/receipts.js',)
js = ('scripts/receipts.js',)
class CostumJSONEncoder(django.core.serializers.json.DjangoJSONEncoder):
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.

def default(self, o):
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
class ReceiptTemplateSelectForm(django.forms.forms.Form):
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.

ReceiptTemplateSelectForm(*args, **kwargs)
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        )
media

Return all media required to render the widgets on this form.

declared_fields = {}
base_fields = {}
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
hidden_fields
visible_fields
get_initial_for_field
django.forms.utils.RenderableFormMixin
as_p
as_table
as_ul
as_div
django.forms.utils.RenderableMixin
render