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

ReceiptForm(*args, project=None, **kwargs)
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
def set_and_clean_general_costs(self, start, end):
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            )
def set_and_clean_department_costs(self, start, end):
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            )
def set_and_clean_project_funded_staff(self, start, end):
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            )
def set_and_clean_salary_level(self, start, end):
 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            )
def check_peroid_overlap(self, start, end):
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            )
def check_closed_periods(self, start, end):
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            )
def set_and_clean_workhours(self, start, end):
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
def clean(self):
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__'.

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

def to_data_dict(self):
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
def to_json(self):
325    def to_json(self):
326        return CostumJSONEncoder().encode(self.to_data_dict())
media

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

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
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:
13    class Meta:
14        model = models.Receipt
15        fields = ['start', 'end', 'receipt_number', 'buper']
16        widgets = {
17            'start': CustomDateInput,
18            'end': CustomDateInput,
19        }
class ReceiptForm.Media:
21    class Media:
22        js = ('scripts/receipts.js',)
class CostumJSONEncoder(django.core.serializers.json.DjangoJSONEncoder):
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.

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

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

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

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
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