payslip.views: 149 total statements, 100.0% covered

Generated: Sun 2014-11-09 09:00 CET

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

Stats: 135 executed, 0 missed, 14 excluded, 231 ignored

  1. """Views for the ``online_docs`` app."""
  2. import cStringIO as StringIO
  3. import os
  4. import pytz
  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 parser, 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 = parser.parse(
  205. self.post_data.get('date_start')).replace(tzinfo=pytz.UTC)
  206. date_end = parser.parse(
  207. self.post_data.get('date_end')).replace(tzinfo=pytz.UTC)
  208. # Get payments for the selected year
  209. payments_year = employee.payments.filter(
  210. # Single payments in this year
  211. Q(date__year=date_start.year, payment_type__rrule__exact='') |
  212. # Recurring payments with past date and end_date in the
  213. # selected year or later
  214. Q(date__lte=date_end, end_date__gte=parser.parse(
  215. '{0}0101T000000'.format(date_start.year)).replace(
  216. tzinfo=pytz.UTC),
  217. payment_type__rrule__isnull=False) |
  218. # Recurring payments with past date in period and open end
  219. Q(date__lte=date_end, end_date__isnull=True,
  220. payment_type__rrule__isnull=False)
  221. )
  222. # Get payments for the selected period
  223. payments = payments_year.filter(
  224. # Single payments in the selected period
  225. Q(date__gte=date_start, date__lte=date_end,
  226. payment_type__rrule__exact='') |
  227. # Recurring payments with past date and end_date in the period
  228. Q(end_date__gte=date_end,
  229. date__lte=date_end, payment_type__rrule__isnull=False) |
  230. # Recurring payments with past date in period and open end
  231. Q(date__lte=date_end, end_date__isnull=True,
  232. payment_type__rrule__isnull=False)
  233. )
  234. # Yearly positive summary
  235. sum_year = payments_year.filter(
  236. amount__gt=0, payment_type__rrule__exact='').aggregate(
  237. Sum('amount')).get('amount__sum') or 0
  238. # Yearly negative summary
  239. sum_year_neg = payments_year.filter(
  240. amount__lt=0, payment_type__rrule__exact='').aggregate(
  241. Sum('amount')).get('amount__sum') or 0
  242. # Yearly summary of recurring payments
  243. for payment in payments_year.exclude(
  244. payment_type__rrule__exact=''):
  245. # If the recurring payment started in a year before, let's take
  246. # January 1st as a start, otherwise take the original date
  247. if payment.date.year < date_start.year:
  248. start = parser.parse('{0}0101T000000'.format(
  249. date_start.year)).replace(tzinfo=pytz.UTC)
  250. else:
  251. start = payment.date
  252. # If the payments ends before the period's end date, let's take
  253. # this date, otherwise we can take the period's end
  254. if payment.end_date and payment.end_date < date_end:
  255. end = payment.end_date
  256. else:
  257. end = date_end
  258. recurrings = rrule.rrule(
  259. rrule._rrulestr._freq_map.get(payment.payment_type.rrule),
  260. dtstart=start, until=end,
  261. )
  262. # Multiply amount with recurrings
  263. if payment.amount > 0:
  264. sum_year += payment.amount * recurrings.count()
  265. else:
  266. sum_year_neg += payment.amount * recurrings.count()
  267. # Period summaries
  268. sum = payments.filter(amount__gt=0).aggregate(
  269. Sum('amount')).get('amount__sum') or 0
  270. sum_neg = payments.filter(amount__lt=0).aggregate(
  271. Sum('amount')).get('amount__sum') or 0
  272. kwargs.update({
  273. 'employee': employee,
  274. 'date_start': date_start,
  275. 'date_end': date_end,
  276. 'payments': payments,
  277. 'payment_extra_fields': ExtraFieldType.objects.filter(
  278. model='Payment'),
  279. 'sum_year': sum_year,
  280. 'sum_year_neg': sum_year + sum_year_neg,
  281. 'sum': sum,
  282. 'sum_neg': sum_neg,
  283. 'currency': CURRENCY,
  284. })
  285. return kwargs
  286. def form_valid(self, form):
  287. self.post_data = self.request.POST
  288. if 'download' in self.post_data:
  289. result = StringIO.StringIO()
  290. html = self.render_to_response(self.get_context_data(form=form))
  291. f = open(os.path.join(
  292. os.path.dirname(__file__), './static/payslip/css/payslip.css'))
  293. pdf = pisa.CreatePDF(html.render().content, result,
  294. default_css=f.read())
  295. f.close()
  296. if not pdf.err:
  297. return HttpResponse(result.getvalue(),
  298. mimetype='application/pdf')
  299. return self.render_to_response(self.get_context_data(form=form))