payslip.views: 150 total statements, 100.0% covered

Generated: Mon 2014-11-10 12:51 CET

Source file: /home/tobi/Projects/django-payslip/src/payslip/views.py

Stats: 136 executed, 0 missed, 14 excluded, 236 ignored

  1. """Views for the ``online_docs`` app."""
  2. import cStringIO as StringIO
  3. from datetime import datetime
  4. import os
  5. from django.contrib.auth.decorators import login_required
  6. from django.core.urlresolvers import reverse
  7. from django.db.models import Q, Sum
  8. from django.http import Http404, HttpResponse
  9. from django.utils.decorators import method_decorator
  10. from django.views.generic import (
  11. CreateView,
  12. DeleteView,
  13. FormView,
  14. TemplateView,
  15. UpdateView,
  16. )
  17. from dateutil import relativedelta, rrule
  18. from xhtml2pdf import pisa
  19. from .app_settings import CURRENCY
  20. from .forms import (
  21. EmployeeForm,
  22. ExtraFieldForm,
  23. PaymentForm,
  24. PayslipForm,
  25. )
  26. from .models import (
  27. Company,
  28. Employee,
  29. ExtraField,
  30. ExtraFieldType,
  31. Payment,
  32. PaymentType,
  33. )
  34. # -------------#
  35. # Mixins #
  36. # -------------#
  37. class PermissionMixin(object):
  38. """Mixin to handle security functions."""
  39. @method_decorator(login_required)
  40. def dispatch(self, request, *args, **kwargs):
  41. """
  42. Makes sure that the user is logged in and has the right to display this
  43. view.
  44. """
  45. if not request.user.is_staff:
  46. raise Http404
  47. return super(PermissionMixin, self).dispatch(request, *args, **kwargs)
  48. def get_success_url(self):
  49. return reverse('payslip_dashboard')
  50. class CompanyMixin(object):
  51. """Mixin to handle company related functions."""
  52. @method_decorator(login_required)
  53. def dispatch(self, request, *args, **kwargs):
  54. """
  55. Makes sure that the user is logged in and has the right to display this
  56. view.
  57. """
  58. self.kwargs = kwargs
  59. self.object = self.get_object()
  60. try:
  61. Employee.objects.get(company=self.object, user=request.user,
  62. is_manager=True)
  63. except Employee.DoesNotExist:
  64. if not request.user.is_staff:
  65. raise Http404
  66. return super(CompanyMixin, self).dispatch(request, *args, **kwargs)
  67. def get_success_url(self):
  68. return reverse('payslip_dashboard')
  69. class CompanyPermissionMixin(object):
  70. """Mixin to handle company-wide permissions functions."""
  71. @method_decorator(login_required)
  72. def dispatch(self, request, *args, **kwargs):
  73. """
  74. Makes sure that the user is logged in and has the right to display this
  75. view.
  76. """
  77. try:
  78. self.company = Employee.objects.get(
  79. user=request.user, is_manager=True).company
  80. except Employee.DoesNotExist:
  81. if not request.user.is_staff:
  82. raise Http404
  83. self.company = None
  84. return super(CompanyPermissionMixin, self).dispatch(request, *args,
  85. **kwargs)
  86. def get_success_url(self):
  87. return reverse('payslip_dashboard')
  88. class EmployeeMixin(object):
  89. """Mixin to handle employee related functions."""
  90. form_class = EmployeeForm
  91. def get_form_kwargs(self):
  92. kwargs = super(EmployeeMixin, self).get_form_kwargs()
  93. kwargs.update({'company': self.company})
  94. return kwargs
  95. class ExtraFieldMixin(object):
  96. """Mixin to handle extra field related functions."""
  97. model = ExtraField
  98. form_class = ExtraFieldForm
  99. class ExtraFieldTypeMixin(object):
  100. """Mixin to handle extra field type related functions."""
  101. model = ExtraFieldType
  102. class PaymentMixin(object):
  103. """Mixin to handle payment related functions."""
  104. model = Payment
  105. form_class = PaymentForm
  106. class PaymentTypeMixin(object):
  107. """Mixin to handle payment type related functions."""
  108. model = PaymentType
  109. # -------------#
  110. # Views #
  111. # -------------#
  112. class DashboardView(PermissionMixin, TemplateView):
  113. """Dashboard to navigate through the payslip app."""
  114. template_name = 'payslip/dashboard.html'
  115. def get_context_data(self, **kwargs):
  116. return {
  117. 'companies': Company.objects.all(),
  118. 'employees': Employee.objects.all(),
  119. 'extra_field_types': ExtraFieldType.objects.all(),
  120. 'fixed_value_extra_fields': ExtraField.objects.filter(
  121. field_type__fixed_values=True),
  122. 'payments': Payment.objects.all(),
  123. 'payment_types': PaymentType.objects.all(),
  124. }
  125. class CompanyCreateView(PermissionMixin, CreateView):
  126. """Classic view to create a company."""
  127. model = Company
  128. def get_success_url(self):
  129. return reverse('payslip_dashboard')
  130. class CompanyUpdateView(CompanyMixin, UpdateView):
  131. """Classic view to update a company."""
  132. model = Company
  133. class CompanyDeleteView(CompanyMixin, DeleteView):
  134. """Classic view to delete a company."""
  135. model = Company
  136. class EmployeeCreateView(CompanyPermissionMixin, EmployeeMixin, CreateView):
  137. """Classic view to create an employee."""
  138. model = Employee
  139. class EmployeeUpdateView(CompanyPermissionMixin, EmployeeMixin, UpdateView):
  140. """Classic view to update an employee."""
  141. model = Employee
  142. class EmployeeDeleteView(CompanyPermissionMixin, EmployeeMixin, DeleteView):
  143. """Classic view to delete an employee."""
  144. model = Employee
  145. class ExtraFieldTypeCreateView(PermissionMixin, ExtraFieldTypeMixin,
  146. CreateView):
  147. """Classic view to create an extra field type."""
  148. pass
  149. class ExtraFieldTypeUpdateView(PermissionMixin, ExtraFieldTypeMixin,
  150. UpdateView):
  151. """Classic view to update an extra field type."""
  152. pass
  153. class ExtraFieldTypeDeleteView(PermissionMixin, ExtraFieldTypeMixin,
  154. DeleteView):
  155. """Classic view to delete an extra field type."""
  156. pass
  157. class ExtraFieldCreateView(PermissionMixin, ExtraFieldMixin, CreateView):
  158. """Classic view to create an extra field."""
  159. pass
  160. class ExtraFieldUpdateView(PermissionMixin, ExtraFieldMixin, UpdateView):
  161. """Classic view to update an extra field."""
  162. pass
  163. class ExtraFieldDeleteView(PermissionMixin, ExtraFieldMixin, DeleteView):
  164. """Classic view to delete an extra field."""
  165. pass
  166. class PaymentTypeCreateView(CompanyPermissionMixin, PaymentTypeMixin,
  167. CreateView):
  168. """Classic view to create a payment type."""
  169. pass
  170. class PaymentTypeUpdateView(CompanyPermissionMixin, PaymentTypeMixin,
  171. UpdateView):
  172. """Classic view to update a payment type."""
  173. pass
  174. class PaymentTypeDeleteView(CompanyPermissionMixin, PaymentTypeMixin,
  175. DeleteView):
  176. """Classic view to delete a payment type."""
  177. pass
  178. class PaymentCreateView(CompanyPermissionMixin, PaymentMixin, CreateView):
  179. """Classic view to create a payment."""
  180. pass
  181. class PaymentUpdateView(CompanyPermissionMixin, PaymentMixin, UpdateView):
  182. """Classic view to update a payment."""
  183. pass
  184. class PaymentDeleteView(CompanyPermissionMixin, PaymentMixin, DeleteView):
  185. """Classic view to delete a payment."""
  186. pass
  187. class PayslipGeneratorView(CompanyPermissionMixin, FormView):
  188. """View to present a small form to generate a custom payslip."""
  189. template_name = 'payslip/payslip_form.html'
  190. form_class = PayslipForm
  191. def get_form_kwargs(self):
  192. kwargs = super(PayslipGeneratorView, self).get_form_kwargs()
  193. kwargs.update({'company': self.company})
  194. return kwargs
  195. def get_template_names(self):
  196. if hasattr(self, 'post_data'):
  197. return ['payslip/payslip.html']
  198. return super(PayslipGeneratorView, self).get_template_names()
  199. def get_context_data(self, **kwargs):
  200. kwargs = super(PayslipGeneratorView, self).get_context_data(**kwargs)
  201. if hasattr(self, 'post_data'):
  202. # Get form data
  203. employee = Employee.objects.get(pk=self.post_data.get('employee'))
  204. date_start = datetime.strptime(
  205. '{}-{}-01'.format(
  206. self.post_data.get('year'), self.post_data.get('month')),
  207. '%Y-%m-%d',
  208. )
  209. january_1st = datetime.strptime(
  210. '{}-01-01'.format(self.post_data.get('year')),
  211. '%Y-%m-%d',
  212. )
  213. date_end = (date_start + relativedelta.relativedelta(months=1)
  214. - relativedelta.relativedelta(days=1))
  215. # Get payments for the selected year
  216. payments_year = employee.payments.filter(
  217. # Single payments in this year
  218. Q(date__year=date_start.year,
  219. payment_type__rrule__isnull=True) |
  220. # Recurring payments with past date and end_date in the
  221. # selected year or later
  222. Q(date__lte=date_end, end_date__gte=january_1st,
  223. payment_type__rrule__isnull=False) |
  224. # Recurring payments with past date in period and open end
  225. Q(date__lte=date_end, end_date__isnull=True,
  226. payment_type__rrule__isnull=False)
  227. )
  228. # Get payments for the selected period
  229. payments = payments_year.exclude(
  230. # Exclude single payments not transferred in the period
  231. Q(date__lt=date_start) |
  232. Q(date__gt=date_end),
  233. Q(payment_type__rrule__exact=''),
  234. ).filter(
  235. # Recurring payments with past date and end_date in the period
  236. Q(end_date__gte=date_end, date__lte=date_end) |
  237. # Recurring payments with past date in period and open end
  238. Q(date__lte=date_end, end_date__isnull=True)
  239. )
  240. # Yearly positive summary
  241. sum_year = payments_year.filter(
  242. amount__gt=0, payment_type__rrule__exact='').aggregate(
  243. Sum('amount')).get('amount__sum') or 0
  244. # Yearly negative summary
  245. sum_year_neg = payments_year.filter(
  246. amount__lt=0, payment_type__rrule__exact='').aggregate(
  247. Sum('amount')).get('amount__sum') or 0
  248. # Yearly summary of recurring payments
  249. for payment in payments_year.exclude(
  250. payment_type__rrule__exact=''):
  251. # If the recurring payment started in a year before, let's take
  252. # January 1st as a start, otherwise take the original date
  253. if payment.get_date_without_tz().year < date_start.year:
  254. start = january_1st
  255. else:
  256. start = payment.get_date_without_tz()
  257. # If the payments ends before the period's end date, let's take
  258. # this date, otherwise we can take the period's end
  259. if (payment.end_date
  260. and payment.get_end_date_without_tz() < date_end):
  261. end = payment.get_end_date_without_tz()
  262. else:
  263. end = date_end
  264. recurrings = rrule.rrule(
  265. rrule._rrulestr._freq_map.get(payment.payment_type.rrule),
  266. dtstart=start, until=end,
  267. )
  268. # Multiply amount with recurrings
  269. if payment.amount > 0:
  270. sum_year += payment.amount * recurrings.count()
  271. else:
  272. sum_year_neg += payment.amount * recurrings.count()
  273. # Period summaries
  274. sum = payments.filter(amount__gt=0).aggregate(
  275. Sum('amount')).get('amount__sum') or 0
  276. sum_neg = payments.filter(amount__lt=0).aggregate(
  277. Sum('amount')).get('amount__sum') or 0
  278. kwargs.update({
  279. 'employee': employee,
  280. 'date_start': date_start,
  281. 'date_end': date_end,
  282. 'payments': payments,
  283. 'payment_extra_fields': ExtraFieldType.objects.filter(
  284. model='Payment'),
  285. 'sum_year': sum_year,
  286. 'sum_year_neg': sum_year + sum_year_neg,
  287. 'sum': sum,
  288. 'sum_neg': sum_neg,
  289. 'currency': CURRENCY,
  290. })
  291. return kwargs
  292. def form_valid(self, form):
  293. self.post_data = self.request.POST
  294. if 'download' in self.post_data:
  295. result = StringIO.StringIO()
  296. html = self.render_to_response(self.get_context_data(form=form))
  297. f = open(os.path.join(
  298. os.path.dirname(__file__), './static/payslip/css/payslip.css'))
  299. pdf = pisa.CreatePDF(html.render().content, result,
  300. default_css=f.read())
  301. f.close()
  302. if not pdf.err:
  303. return HttpResponse(result.getvalue(),
  304. mimetype='application/pdf')
  305. return self.render_to_response(self.get_context_data(form=form))