├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG.txt ├── DESCRIPTION ├── LICENSE ├── MANIFEST.in ├── README.rst ├── manage.py ├── payslip ├── __init__.py ├── admin.py ├── app_settings.py ├── fixtures │ └── bootstrap_auth.json ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── south_migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_field_employee_is_manager.py │ ├── 0003_auto__add_field_extrafieldtype_model__add_field_extrafieldtype_fixed_v.py │ ├── 0004_auto__add_field_paymenttype_rrule__chg_field_extrafieldtype_model.py │ ├── 0005_auto__add_field_payment_end_date.py │ └── __init__.py ├── static │ └── payslip │ │ ├── css │ │ ├── base.css │ │ └── payslip.css │ │ └── js │ │ └── payslip.js ├── templates │ └── payslip │ │ ├── company_confirm_delete.html │ │ ├── company_form.html │ │ ├── dashboard.html │ │ ├── delete_base.html │ │ ├── employee_confirm_delete.html │ │ ├── employee_form.html │ │ ├── extrafield_confirm_delete.html │ │ ├── extrafield_form.html │ │ ├── extrafieldtype_confirm_delete.html │ │ ├── extrafieldtype_form.html │ │ ├── payment_confirm_delete.html │ │ ├── payment_form.html │ │ ├── paymenttype_confirm_delete.html │ │ ├── paymenttype_form.html │ │ ├── payslip.html │ │ ├── payslip_base.html │ │ └── payslip_form.html ├── templatetags │ ├── __init__.py │ └── payslip_tags.py ├── tests │ ├── __init__.py │ ├── forms_tests.py │ ├── models_tests.py │ ├── settings.py │ ├── tag_tests.py │ ├── test_app │ │ ├── __init__.py │ │ ├── models.py │ │ └── templates │ │ │ ├── 404.html │ │ │ └── 500.html │ ├── test_settings.py │ ├── urls.py │ └── views_tests.py ├── urls.py └── views.py ├── requirements.txt ├── runtests.py ├── setup.py ├── test_requirements.txt └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | .tox 4 | .coverage 5 | coverage/ 6 | db.sqlite 7 | dist/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | install: pip install -r requirements.txt --use-mirrors 6 | script: python payslip/tests/runtests.py 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Current or previous core committers 2 | 3 | Tobias Lorenz 4 | 5 | Contributors (in alphabetical order) 6 | 7 | * Your name could stand here :) 8 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | === 0.3.X (ongoing) === 2 | 3 | - Added proper pdf filename 4 | - Prepared app for Django 1.9 and Python 3.5 5 | - Upgrade to Django>=1.8 6 | 7 | === 0.2 === 8 | 9 | - Improved dashboard template 10 | - Improved payslip form 11 | - Fixed payment querysets 12 | - Changed to weasyprint 13 | 14 | === 0.2 === 15 | 16 | - Made use of timezones 17 | - updated requirements and templates 18 | - Fixed single payment generator 19 | 20 | === 0.1 === 21 | 22 | Initial commit 23 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | A reusable Django app that allows you to enter salary data of your employees 2 | so that authorized persons can view, print and export (PDF) their payslips. 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2012 Martin Brochhaus 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include DESCRIPTION 4 | include CHANGELOG.txt 5 | include README.md 6 | graft payslip 7 | global-exclude *.orig *.pyc *.log 8 | prune payslip/tests/coverage/ 9 | prune payslip/.ropeproject/ 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Payslip 2 | ============== 3 | 4 | A reusable Django app that allows you to enter salary data of your employees 5 | so that authorized persons can view, print and export (PDF) their payslips. 6 | 7 | Installation 8 | ------------ 9 | 10 | You need to install the following prerequisites in order to use this app:: 11 | 12 | pip install Django 13 | pip install django-libs 14 | pip install python-dateutil 15 | pip install WeasyPrint 16 | 17 | If you want to install the latest stable release from PyPi:: 18 | 19 | $ pip install django-payslip 20 | 21 | If you feel adventurous and want to install the latest commit from GitHub:: 22 | 23 | $ pip install -e git://github.com/bitmazk/django-payslip.git#egg=payslip 24 | 25 | Add ``payslip`` to your ``INSTALLED_APPS``:: 26 | 27 | INSTALLED_APPS = ( 28 | ..., 29 | 'payslip', 30 | ) 31 | 32 | Hook this app into your ``urls.py``:: 33 | 34 | urlpatterns = patterns('', 35 | ... 36 | url(r'^payslip/', include('payslip.urls')), 37 | ) 38 | 39 | 40 | Features & Usage 41 | ---------------- 42 | 43 | You can: 44 | 45 | * Define companies 46 | * Assign employees (a user model is created automatically) 47 | * Define payment types 48 | * Create payments 49 | * Create custom extra fields for COMPANIES, EMPLOYEES and PAYMENTS 50 | * Create global attributes for those custom fields (dropdown fields) 51 | * Generate custom payslips 52 | * Print those payslips or export them as styled PDF documents 53 | 54 | There's already a print-ready template for your payslips, which should cover 55 | mainly used payslips. If you want to you can override the template with your 56 | own styles. Find it here ``payslip/templates/payslip/payslip.html``. 57 | 58 | You can also create your own CSS, but be sure to cover print styles. Find it 59 | here ``static/payslip/css/payslip.css``. 60 | 61 | After you have added the basic company information needed in your template, you 62 | can add payments and employees and start paysliping. :) Have fun with it. 63 | 64 | 65 | Settings 66 | -------- 67 | 68 | PAYSLIP_CURRENCY 69 | ++++++++++++++++ 70 | 71 | Default: 'EUR' 72 | 73 | Your preferred currency acronym. 74 | 75 | 76 | Contribute 77 | ---------- 78 | 79 | If you want to contribute to this project, please perform the following steps:: 80 | 81 | # Fork this repository 82 | # Clone your fork 83 | $ mkvirtualenv -p python2.7 payslip 84 | $ pip install -r requirements.txt 85 | $ ./payslip/tests/runtests.sh 86 | # You should get no failing tests 87 | 88 | $ git co -b feature_branch master 89 | # Implement your feature and tests 90 | # Describe your change in the CHANGELOG.txt 91 | $ git add . && git commit 92 | $ git push origin feature_branch 93 | # Send us a pull request for your feature branch 94 | 95 | Whenever you run the tests a coverage output will be generated in 96 | ``tests/coverage/index.html``. When adding new features, please make sure that 97 | you keep the coverage at 100%. 98 | 99 | If you are making changes that need to be tested in a browser (i.e. to the 100 | CSS or JS files), you might want to setup a Django project, follow the 101 | installation instructions above, then run ``python setup.py develop``. This 102 | will just place an egg-link to your cloned fork in your project's virtualenv. 103 | 104 | Roadmap 105 | ------- 106 | 107 | * Add employee and manager dashboards 108 | 109 | Check the issue tracker on github for further milestones and features to come. 110 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", 7 | "payslip.tests.settings") 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /payslip/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = '0.3.2' 3 | -------------------------------------------------------------------------------- /payslip/admin.py: -------------------------------------------------------------------------------- 1 | """Admin classes for the payslip app.""" 2 | from django.contrib import admin 3 | 4 | from . import models 5 | 6 | 7 | admin.site.register(models.Company) 8 | admin.site.register(models.Employee) 9 | admin.site.register(models.ExtraField) 10 | admin.site.register(models.ExtraFieldType) 11 | admin.site.register(models.Payment) 12 | admin.site.register(models.PaymentType) 13 | -------------------------------------------------------------------------------- /payslip/app_settings.py: -------------------------------------------------------------------------------- 1 | """Settings of the ``payslip``` application.""" 2 | from django.conf import settings 3 | 4 | CURRENCY = getattr(settings, 'PAYSLIP_CURRENCY', 'EUR') 5 | -------------------------------------------------------------------------------- /payslip/fixtures/bootstrap_auth.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "admin", 7 | "first_name": "", 8 | "last_name": "", 9 | "is_active": true, 10 | "is_superuser": true, 11 | "is_staff": true, 12 | "last_login": "2012-11-26T17:04:43", 13 | "groups": [], 14 | "user_permissions": [], 15 | "password": "md5$iL4a3g0DV1Et$7ee95f30b9a4946ed09cca43505b2709", 16 | "email": "info@example.com", 17 | "date_joined": "2012-06-16T10:38:31" 18 | } 19 | } 20 | ] -------------------------------------------------------------------------------- /payslip/forms.py: -------------------------------------------------------------------------------- 1 | """Forms for the ``payslip`` app.""" 2 | import hashlib 3 | 4 | from django.contrib.auth import get_user_model 5 | from django.contrib.auth.hashers import make_password 6 | from django.db.models import Q 7 | from django import forms 8 | from django.utils import timezone 9 | from django.utils.translation import ugettext_lazy as _ 10 | 11 | from dateutil.relativedelta import relativedelta 12 | 13 | from .models import ( 14 | Company, 15 | Employee, 16 | ExtraField, 17 | ExtraFieldType, 18 | Payment, 19 | ) 20 | 21 | 22 | def get_md5_hexdigest(email): 23 | """ 24 | Returns an md5 hash for a given email. 25 | 26 | The length is 30 so that it fits into Django's ``User.username`` field. 27 | 28 | """ 29 | h = hashlib.new('md5') 30 | h.update(email.encode('utf-8')) 31 | return h.hexdigest()[0:30] 32 | 33 | 34 | def generate_username(email): 35 | """ 36 | Generates a unique username for the given email. 37 | 38 | The username will be an md5 hash of the given email. If the username exists 39 | we just append `a` to the email until we get a unique md5 hash. 40 | 41 | """ 42 | username = get_md5_hexdigest(email) 43 | found_unique_username = False 44 | while not found_unique_username: 45 | try: 46 | get_user_model().objects.get(username=username) 47 | email = '{0}a'.format(email) 48 | username = get_md5_hexdigest(email) 49 | except get_user_model().DoesNotExist: 50 | found_unique_username = True 51 | return username 52 | 53 | 54 | class ExtraFieldFormMixin(object): 55 | """Mixin to handle extra field related functions.""" 56 | def __init__(self, *args, **kwargs): 57 | self.extra_field_types = ExtraFieldType.objects.filter( 58 | Q(model=self.Meta.model.__name__) | Q(model__isnull=True)) 59 | if kwargs.get('instance'): 60 | for extra_field_type in self.extra_field_types: 61 | try: 62 | field = kwargs.get('instance').extra_fields.get( 63 | field_type__name=extra_field_type.name) 64 | except ExtraField.DoesNotExist: 65 | pass 66 | else: 67 | kwargs['initial'].update({'{0}'.format( 68 | extra_field_type.name): field.value}) 69 | super(ExtraFieldFormMixin, self).__init__(*args, **kwargs) 70 | for extra_field_type in self.extra_field_types: 71 | if extra_field_type.fixed_values: 72 | choices = [(x.value, x.value) 73 | for x in extra_field_type.extra_fields.all()] 74 | choices.append(('', '-----')) 75 | self.fields[extra_field_type.name] = forms.ChoiceField( 76 | required=False, 77 | choices=list(set(choices)), 78 | ) 79 | else: 80 | self.fields[extra_field_type.name] = forms.CharField( 81 | required=False, max_length=200) 82 | 83 | def save(self, *args, **kwargs): 84 | resp = super(ExtraFieldFormMixin, self).save(*args, **kwargs) 85 | for extra_field_type in self.extra_field_types: 86 | try: 87 | field_to_save = self.instance.extra_fields.get( 88 | field_type__name=extra_field_type.name) 89 | except ExtraField.DoesNotExist: 90 | field_to_save = None 91 | if extra_field_type.fixed_values: 92 | if field_to_save: 93 | self.instance.extra_fields.remove( 94 | self.instance.extra_fields.get( 95 | field_type__name=extra_field_type.name)) 96 | try: 97 | field_to_save = ExtraField.objects.get( 98 | field_type__name=extra_field_type.name, 99 | value=self.data.get(extra_field_type.name)) 100 | except ExtraField.DoesNotExist: 101 | pass 102 | else: 103 | self.instance.extra_fields.add(field_to_save) 104 | else: 105 | if field_to_save and self.data.get(extra_field_type.name): 106 | field_to_save.value = self.data[extra_field_type.name] 107 | field_to_save.save() 108 | elif self.data.get(extra_field_type.name): 109 | new_field = ExtraField( 110 | field_type=extra_field_type, 111 | value=self.data.get(extra_field_type.name), 112 | ) 113 | new_field.save() 114 | self.instance.extra_fields.add(new_field) 115 | return resp 116 | 117 | 118 | class EmployeeForm(ExtraFieldFormMixin, forms.ModelForm): 119 | """Form to create a new Employee instance.""" 120 | first_name = forms.CharField(max_length=30) 121 | last_name = forms.CharField(max_length=30) 122 | email = forms.EmailField() 123 | password = forms.CharField(widget=forms.PasswordInput(), max_length=128) 124 | retype_password = forms.CharField(widget=forms.PasswordInput(), 125 | max_length=128) 126 | 127 | def __init__(self, company, *args, **kwargs): 128 | self.company = company 129 | if kwargs.get('instance'): 130 | instance = kwargs.get('instance') 131 | user = instance.user 132 | kwargs['initial'] = { 133 | 'first_name': user.first_name, 134 | 'last_name': user.last_name, 135 | 'email': user.email, 136 | } 137 | super(EmployeeForm, self).__init__(*args, **kwargs) 138 | if self.instance.id: 139 | del self.fields['password'] 140 | del self.fields['retype_password'] 141 | if self.company and self.company.pk: 142 | del self.fields['company'] 143 | 144 | def clean_email(self): 145 | """ 146 | Validate that the username is alphanumeric and is not already 147 | in use. 148 | 149 | """ 150 | email = self.cleaned_data['email'] 151 | try: 152 | user = get_user_model().objects.get(email__iexact=email) 153 | except get_user_model().DoesNotExist: 154 | return email 155 | if self.instance.id and user == self.instance.user: 156 | return email 157 | raise forms.ValidationError( 158 | _('A user with that email already exists.')) 159 | 160 | def clean(self): 161 | """ 162 | Verifiy that the values entered into the two password fields match. 163 | 164 | Note that an error here will end up in ``non_field_errors()`` because 165 | it doesn't apply to a single field. 166 | 167 | """ 168 | data = self.cleaned_data 169 | if 'email' not in data: 170 | return data 171 | if ('password' in data and 'retype_password' in data): 172 | if data['password'] != data['retype_password']: 173 | raise forms.ValidationError( 174 | _("The two password fields didn't match.")) 175 | 176 | self.cleaned_data['username'] = generate_username(data['email']) 177 | return self.cleaned_data 178 | 179 | def save(self, *args, **kwargs): 180 | if self.instance.id: 181 | get_user_model().objects.filter(pk=self.instance.user.pk).update( 182 | first_name=self.cleaned_data.get('first_name'), 183 | last_name=self.cleaned_data.get('last_name'), 184 | email=self.cleaned_data.get('email'), 185 | ) 186 | else: 187 | user = get_user_model().objects.create( 188 | username=self.cleaned_data.get('email'), 189 | first_name=self.cleaned_data.get('first_name'), 190 | last_name=self.cleaned_data.get('last_name'), 191 | email=self.cleaned_data.get('email'), 192 | password=make_password(self.cleaned_data.get('password')), 193 | ) 194 | self.instance.user = user 195 | if self.company and self.company.pk: 196 | self.instance.company = Company.objects.get(pk=self.company.pk) 197 | return super(EmployeeForm, self).save(*args, **kwargs) 198 | 199 | class Meta: 200 | model = Employee 201 | fields = ('company', 'hr_number', 'address', 'title', 'is_manager') 202 | 203 | 204 | class PaymentForm(ExtraFieldFormMixin, forms.ModelForm): 205 | """Form to create a new Payment instance.""" 206 | class Meta: 207 | model = Payment 208 | fields = ('payment_type', 'employee', 'amount', 'date', 'end_date', 209 | 'description') 210 | 211 | 212 | class ExtraFieldForm(forms.ModelForm): 213 | """Form to create a new ExtraField instance.""" 214 | def __init__(self, *args, **kwargs): 215 | super(ExtraFieldForm, self).__init__(*args, **kwargs) 216 | self.fields['field_type'].queryset = ExtraFieldType.objects.filter( 217 | fixed_values=True) 218 | 219 | class Meta: 220 | model = ExtraField 221 | fields = '__all__' 222 | 223 | 224 | class PayslipForm(forms.Form): 225 | """Form to create a custom payslip.""" 226 | year = forms.ChoiceField() 227 | month = forms.ChoiceField() 228 | employee = forms.ChoiceField() 229 | 230 | def __init__(self, company, *args, **kwargs): 231 | super(PayslipForm, self).__init__(*args, **kwargs) 232 | last_month = timezone.now().replace(day=1) - relativedelta(months=1) 233 | self.fields['month'].choices = ( 234 | (1, _('January')), 235 | (2, _('February')), 236 | (3, _('March')), 237 | (4, _('April')), 238 | (5, _('May')), 239 | (6, _('June')), 240 | (7, _('July')), 241 | (8, _('August')), 242 | (9, _('September')), 243 | (10, _('October')), 244 | (11, _('November')), 245 | (12, _('December')), 246 | ) 247 | self.fields['month'].initial = last_month.month 248 | current_year = timezone.now().year 249 | self.fields['year'].choices = [ 250 | (current_year - x, current_year - x) for x in range(0, 20)] 251 | self.fields['year'].initial = last_month.year 252 | self.company = company 253 | if self.company: 254 | self.fields['employee'].choices = [( 255 | x.id, x) for x in self.company.employees.all()] 256 | else: 257 | self.fields['employee'].choices = [( 258 | x.id, x) for x in Employee.objects.all()] 259 | -------------------------------------------------------------------------------- /payslip/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.5 on 2016-04-29 10:51 3 | from __future__ import unicode_literals 4 | 5 | import datetime 6 | from django.conf import settings 7 | from django.db import migrations, models 8 | import django.db.models.deletion 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='Company', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('name', models.CharField(max_length=100, verbose_name='Name')), 25 | ('address', models.TextField(blank=True, null=True, verbose_name='Address')), 26 | ], 27 | options={ 28 | 'ordering': ['name'], 29 | }, 30 | ), 31 | migrations.CreateModel( 32 | name='Employee', 33 | fields=[ 34 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('hr_number', models.PositiveIntegerField(blank=True, null=True, verbose_name='HR number')), 36 | ('address', models.TextField(blank=True, null=True, verbose_name='Address')), 37 | ('title', models.CharField(choices=[(b'1', b'Ms.'), (b'2', b'Mrs.'), (b'3', b'Mr.'), (b'4', b'Dr.')], max_length=1, verbose_name='Title')), 38 | ('is_manager', models.BooleanField(default=False, verbose_name='is Manager')), 39 | ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='employees', to='payslip.Company', verbose_name='Company')), 40 | ], 41 | options={ 42 | 'ordering': ['company__name', 'user__first_name'], 43 | }, 44 | ), 45 | migrations.CreateModel( 46 | name='ExtraField', 47 | fields=[ 48 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 49 | ('value', models.CharField(max_length=200, verbose_name='Value')), 50 | ], 51 | options={ 52 | 'ordering': ['field_type__name'], 53 | }, 54 | ), 55 | migrations.CreateModel( 56 | name='ExtraFieldType', 57 | fields=[ 58 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 59 | ('name', models.CharField(max_length=100, verbose_name='Name')), 60 | ('description', models.CharField(blank=True, max_length=100, null=True, verbose_name='Description')), 61 | ('model', models.CharField(blank=True, choices=[(b'Employee', b'Employee'), (b'Payment', b'Payment'), (b'Company', b'Company')], max_length=10, null=True, verbose_name='Model')), 62 | ('fixed_values', models.BooleanField(default=False, verbose_name='Fixed values')), 63 | ], 64 | options={ 65 | 'ordering': ['name'], 66 | }, 67 | ), 68 | migrations.CreateModel( 69 | name='Payment', 70 | fields=[ 71 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 72 | ('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Amount')), 73 | ('date', models.DateTimeField(default=datetime.datetime(2016, 4, 29, 5, 51, 42, 30178), verbose_name='Date')), 74 | ('end_date', models.DateTimeField(blank=True, help_text='This field is only considered, if the payment type has a recurring rule.', null=True, verbose_name='End of recurring period')), 75 | ('description', models.CharField(blank=True, max_length=100, null=True, verbose_name='Description')), 76 | ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='payslip.Employee', verbose_name='Employee')), 77 | ('extra_fields', models.ManyToManyField(blank=True, null=True, to='payslip.ExtraField', verbose_name='Extra fields')), 78 | ], 79 | options={ 80 | 'ordering': ['employee__user__first_name', '-date'], 81 | }, 82 | ), 83 | migrations.CreateModel( 84 | name='PaymentType', 85 | fields=[ 86 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 87 | ('name', models.CharField(max_length=100, verbose_name='Name')), 88 | ('rrule', models.CharField(blank=True, choices=[(b'MONTHLY', 'Monthly'), (b'YEARLY', 'Yearly')], max_length=10, verbose_name='Recurring rule')), 89 | ('description', models.CharField(blank=True, max_length=100, null=True, verbose_name='Description')), 90 | ], 91 | options={ 92 | 'ordering': ['name'], 93 | }, 94 | ), 95 | migrations.AddField( 96 | model_name='payment', 97 | name='payment_type', 98 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='payslip.PaymentType', verbose_name='Payment type'), 99 | ), 100 | migrations.AddField( 101 | model_name='extrafield', 102 | name='field_type', 103 | field=models.ForeignKey(help_text='Only field types with fixed values can be chosen to add global values.', on_delete=django.db.models.deletion.CASCADE, related_name='extra_fields', to='payslip.ExtraFieldType', verbose_name='Field type'), 104 | ), 105 | migrations.AddField( 106 | model_name='employee', 107 | name='extra_fields', 108 | field=models.ManyToManyField(blank=True, null=True, to='payslip.ExtraField', verbose_name='Extra fields'), 109 | ), 110 | migrations.AddField( 111 | model_name='employee', 112 | name='user', 113 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='employees', to=settings.AUTH_USER_MODEL, verbose_name='User'), 114 | ), 115 | migrations.AddField( 116 | model_name='company', 117 | name='extra_fields', 118 | field=models.ManyToManyField(blank=True, null=True, to='payslip.ExtraField', verbose_name='Extra fields'), 119 | ), 120 | ] 121 | -------------------------------------------------------------------------------- /payslip/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-payslip/55df83e9cba893f63b9380b23a74784d169972cb/payslip/migrations/__init__.py -------------------------------------------------------------------------------- /payslip/models.py: -------------------------------------------------------------------------------- 1 | """Models for the ``payslip`` application.""" 2 | from django.conf import settings 3 | from django.db import models 4 | from django.utils.timezone import localtime, now 5 | from django.utils.encoding import python_2_unicode_compatible 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | 9 | @python_2_unicode_compatible 10 | class Company(models.Model): 11 | """ 12 | Model, which holds general information of a company. 13 | 14 | :name: Name of the company. 15 | :address: Full address model fields. 16 | :extra_fields: Custom fields to hold more information. 17 | 18 | """ 19 | name = models.CharField( 20 | max_length=100, 21 | verbose_name=_('Name'), 22 | ) 23 | 24 | address = models.TextField( 25 | verbose_name=_('Address'), 26 | blank=True, null=True, 27 | ) 28 | 29 | extra_fields = models.ManyToManyField( 30 | 'payslip.ExtraField', 31 | verbose_name=_('Extra fields'), 32 | blank=True, 33 | ) 34 | 35 | class Meta: 36 | ordering = ['name', ] 37 | 38 | def __str__(self): 39 | return '{0}'.format(self.name) 40 | 41 | 42 | @python_2_unicode_compatible 43 | class Employee(models.Model): 44 | """ 45 | Model, which holds personal information of employee. 46 | 47 | :user: Connection to the django user model, to allow a login. 48 | :company: Connection to the employee's company. 49 | :hr_number: ID to connect some non-digital documents or to use as a 50 | reference. 51 | :address: Full address model fields. 52 | :title: Title of the employee. 53 | :extra_fields: Custom fields like e.g. confession, tax class. 54 | 55 | """ 56 | user = models.ForeignKey( 57 | settings.AUTH_USER_MODEL, 58 | verbose_name=_('User'), 59 | related_name='employees', 60 | ) 61 | 62 | company = models.ForeignKey( 63 | 'payslip.Company', 64 | verbose_name=_('Company'), 65 | related_name='employees', 66 | ) 67 | 68 | hr_number = models.PositiveIntegerField( 69 | verbose_name=_('HR number'), 70 | blank=True, null=True, 71 | ) 72 | 73 | address = models.TextField( 74 | verbose_name=_('Address'), 75 | blank=True, null=True, 76 | ) 77 | 78 | title = models.CharField( 79 | max_length=1, 80 | verbose_name=_('Title'), 81 | choices=( 82 | ('1', 'Ms.'), 83 | ('2', 'Mrs.'), 84 | ('3', 'Mr.'), 85 | ('4', 'Dr.'), 86 | ) 87 | ) 88 | 89 | extra_fields = models.ManyToManyField( 90 | 'payslip.ExtraField', 91 | verbose_name=_('Extra fields'), 92 | blank=True, 93 | ) 94 | 95 | is_manager = models.BooleanField( 96 | default=False, 97 | verbose_name=_('is Manager'), 98 | ) 99 | 100 | class Meta: 101 | ordering = ['company__name', 'user__first_name', ] 102 | 103 | def __str__(self): 104 | return '{0} {1}'.format(self.user.first_name, self.user.last_name) 105 | 106 | 107 | @python_2_unicode_compatible 108 | class ExtraFieldType(models.Model): 109 | """ 110 | Model to create custom information holders. 111 | 112 | :name: Name of the attribute. 113 | :description: Description of the attribute. 114 | :model: Can be set in order to allow the use of only one model. 115 | :fixed_values: Can transform related exta fields into choices. 116 | 117 | """ 118 | name = models.CharField( 119 | max_length=100, 120 | verbose_name=_('Name'), 121 | ) 122 | 123 | description = models.CharField( 124 | max_length=100, 125 | blank=True, null=True, 126 | verbose_name=_('Description'), 127 | ) 128 | 129 | model = models.CharField( 130 | max_length=10, 131 | choices=( 132 | ('Employee', 'Employee'), 133 | ('Payment', 'Payment'), 134 | ('Company', 'Company'), 135 | ), 136 | verbose_name=_('Model'), 137 | blank=True, null=True, 138 | ) 139 | 140 | fixed_values = models.BooleanField( 141 | default=False, 142 | verbose_name=_('Fixed values'), 143 | ) 144 | 145 | class Meta: 146 | ordering = ['name', ] 147 | 148 | def __str__(self): 149 | return '{0}'.format(self.name) 150 | 151 | 152 | @python_2_unicode_compatible 153 | class ExtraField(models.Model): 154 | """ 155 | Model to create custom fields. 156 | 157 | :field_type: Connection to the field type. 158 | :value: Current value of this extra field. 159 | 160 | """ 161 | field_type = models.ForeignKey( 162 | 'payslip.ExtraFieldType', 163 | verbose_name=_('Field type'), 164 | related_name='extra_fields', 165 | help_text=_('Only field types with fixed values can be chosen to add' 166 | ' global values.'), 167 | ) 168 | 169 | value = models.CharField( 170 | max_length=200, 171 | verbose_name=_('Value'), 172 | ) 173 | 174 | class Meta: 175 | ordering = ['field_type__name', ] 176 | 177 | def __str__(self): 178 | return '{0} ({1}) - {2}'.format( 179 | self.field_type, self.field_type.get_model_display() or 'general', 180 | self.value) 181 | 182 | 183 | @python_2_unicode_compatible 184 | class PaymentType(models.Model): 185 | """ 186 | Model to create payment types. 187 | 188 | :name: Name of the type. 189 | :rrule: Recurring rule setting. 190 | :description: Description of the type. 191 | 192 | """ 193 | name = models.CharField( 194 | max_length=100, 195 | verbose_name=_('Name'), 196 | ) 197 | 198 | rrule = models.CharField( 199 | max_length=10, 200 | verbose_name=_('Recurring rule'), 201 | blank=True, 202 | choices=( 203 | ('MONTHLY', _('Monthly')), 204 | ('YEARLY', _('Yearly')), 205 | ) 206 | ) 207 | 208 | description = models.CharField( 209 | max_length=100, 210 | blank=True, null=True, 211 | verbose_name=_('Description'), 212 | ) 213 | 214 | class Meta: 215 | ordering = ['name', ] 216 | 217 | def __str__(self): 218 | if self.rrule: 219 | return '{0} ({1})'.format(self.name, self.get_rrule_display()) 220 | return '{0}'.format(self.name) 221 | 222 | 223 | @python_2_unicode_compatible 224 | class Payment(models.Model): 225 | """ 226 | Model, which represents one single payment. 227 | 228 | :payment_type: Type of the payment. 229 | :employee: Connection to the payment receiver. 230 | :amount: Current amount of the payment. 231 | :date: Date the payment should accrue. 232 | :end_date: Optional end date, if payment type has a rrule. 233 | :extra_fields: Custom fields like e.g. quantity, bonus. 234 | 235 | """ 236 | payment_type = models.ForeignKey( 237 | 'payslip.PaymentType', 238 | verbose_name=_('Payment type'), 239 | related_name='payments', 240 | ) 241 | 242 | employee = models.ForeignKey( 243 | 'payslip.Employee', 244 | verbose_name=_('Employee'), 245 | related_name='payments', 246 | ) 247 | 248 | amount = models.DecimalField( 249 | decimal_places=2, 250 | max_digits=10, 251 | verbose_name=_('Amount'), 252 | ) 253 | 254 | date = models.DateTimeField( 255 | verbose_name=_('Date'), 256 | default=now().today(), 257 | ) 258 | 259 | end_date = models.DateTimeField( 260 | verbose_name=_('End of recurring period'), 261 | blank=True, null=True, 262 | help_text=_('This field is only considered, if the payment type has a' 263 | ' recurring rule.'), 264 | ) 265 | 266 | extra_fields = models.ManyToManyField( 267 | 'payslip.ExtraField', 268 | verbose_name=_('Extra fields'), 269 | blank=True, 270 | ) 271 | 272 | description = models.CharField( 273 | max_length=100, 274 | blank=True, null=True, 275 | verbose_name=_('Description'), 276 | ) 277 | 278 | class Meta: 279 | ordering = ['employee__user__first_name', '-date', ] 280 | 281 | def __str__(self): 282 | return '{0} - {1} ({2})'.format(self.payment_type, self.amount, 283 | self.employee) 284 | 285 | def get_date_without_tz(self): 286 | return localtime(self.date).replace(tzinfo=None) 287 | 288 | def get_end_date_without_tz(self): 289 | return localtime(self.end_date).replace(tzinfo=None) 290 | 291 | @property 292 | def is_recurring(self): 293 | return self.payment_type.rrule 294 | -------------------------------------------------------------------------------- /payslip/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | class Migration(SchemaMigration): 10 | 11 | def forwards(self, orm): 12 | # Adding model 'Company' 13 | db.create_table('payslip_company', ( 14 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 15 | ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), 16 | ('address', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), 17 | )) 18 | db.send_create_signal('payslip', ['Company']) 19 | 20 | # Adding M2M table for field extra_fields on 'Company' 21 | db.create_table('payslip_company_extra_fields', ( 22 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), 23 | ('company', models.ForeignKey(orm['payslip.company'], null=False)), 24 | ('extrafield', models.ForeignKey(orm['payslip.extrafield'], null=False)) 25 | )) 26 | db.create_unique('payslip_company_extra_fields', ['company_id', 'extrafield_id']) 27 | 28 | # Adding model 'Employee' 29 | db.create_table('payslip_employee', ( 30 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 31 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='employees', to=orm['auth.User'])), 32 | ('company', self.gf('django.db.models.fields.related.ForeignKey')(related_name='employees', to=orm['payslip.Company'])), 33 | ('hr_number', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)), 34 | ('address', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), 35 | ('title', self.gf('django.db.models.fields.CharField')(max_length=1)), 36 | )) 37 | db.send_create_signal('payslip', ['Employee']) 38 | 39 | # Adding M2M table for field extra_fields on 'Employee' 40 | db.create_table('payslip_employee_extra_fields', ( 41 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), 42 | ('employee', models.ForeignKey(orm['payslip.employee'], null=False)), 43 | ('extrafield', models.ForeignKey(orm['payslip.extrafield'], null=False)) 44 | )) 45 | db.create_unique('payslip_employee_extra_fields', ['employee_id', 'extrafield_id']) 46 | 47 | # Adding model 'ExtraFieldType' 48 | db.create_table('payslip_extrafieldtype', ( 49 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 50 | ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), 51 | ('description', self.gf('django.db.models.fields.CharField')(max_length=100, null=True, blank=True)), 52 | )) 53 | db.send_create_signal('payslip', ['ExtraFieldType']) 54 | 55 | # Adding model 'ExtraField' 56 | db.create_table('payslip_extrafield', ( 57 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 58 | ('field_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='extra_fields', to=orm['payslip.ExtraFieldType'])), 59 | ('value', self.gf('django.db.models.fields.CharField')(max_length=200)), 60 | )) 61 | db.send_create_signal('payslip', ['ExtraField']) 62 | 63 | # Adding model 'PaymentType' 64 | db.create_table('payslip_paymenttype', ( 65 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 66 | ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), 67 | ('description', self.gf('django.db.models.fields.CharField')(max_length=100, null=True, blank=True)), 68 | )) 69 | db.send_create_signal('payslip', ['PaymentType']) 70 | 71 | # Adding model 'Payment' 72 | db.create_table('payslip_payment', ( 73 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 74 | ('payment_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='payments', to=orm['payslip.PaymentType'])), 75 | ('employee', self.gf('django.db.models.fields.related.ForeignKey')(related_name='payments', to=orm['payslip.Employee'])), 76 | ('amount', self.gf('django.db.models.fields.DecimalField')(max_digits=10, decimal_places=2)), 77 | ('date', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2013, 1, 5, 0, 0))), 78 | ('description', self.gf('django.db.models.fields.CharField')(max_length=100, null=True, blank=True)), 79 | )) 80 | db.send_create_signal('payslip', ['Payment']) 81 | 82 | # Adding M2M table for field extra_fields on 'Payment' 83 | db.create_table('payslip_payment_extra_fields', ( 84 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), 85 | ('payment', models.ForeignKey(orm['payslip.payment'], null=False)), 86 | ('extrafield', models.ForeignKey(orm['payslip.extrafield'], null=False)) 87 | )) 88 | db.create_unique('payslip_payment_extra_fields', ['payment_id', 'extrafield_id']) 89 | 90 | 91 | def backwards(self, orm): 92 | # Deleting model 'Company' 93 | db.delete_table('payslip_company') 94 | 95 | # Removing M2M table for field extra_fields on 'Company' 96 | db.delete_table('payslip_company_extra_fields') 97 | 98 | # Deleting model 'Employee' 99 | db.delete_table('payslip_employee') 100 | 101 | # Removing M2M table for field extra_fields on 'Employee' 102 | db.delete_table('payslip_employee_extra_fields') 103 | 104 | # Deleting model 'ExtraFieldType' 105 | db.delete_table('payslip_extrafieldtype') 106 | 107 | # Deleting model 'ExtraField' 108 | db.delete_table('payslip_extrafield') 109 | 110 | # Deleting model 'PaymentType' 111 | db.delete_table('payslip_paymenttype') 112 | 113 | # Deleting model 'Payment' 114 | db.delete_table('payslip_payment') 115 | 116 | # Removing M2M table for field extra_fields on 'Payment' 117 | db.delete_table('payslip_payment_extra_fields') 118 | 119 | 120 | models = { 121 | 'auth.group': { 122 | 'Meta': {'object_name': 'Group'}, 123 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 124 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 125 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 126 | }, 127 | 'auth.permission': { 128 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 129 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 130 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 131 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 132 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 133 | }, 134 | 'auth.user': { 135 | 'Meta': {'object_name': 'User'}, 136 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 137 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 138 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 139 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 140 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 141 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 142 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 143 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 144 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 145 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 146 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 147 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 148 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 149 | }, 150 | 'contenttypes.contenttype': { 151 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 152 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 153 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 154 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 155 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 156 | }, 157 | 'payslip.company': { 158 | 'Meta': {'object_name': 'Company'}, 159 | 'address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 160 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 161 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 162 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 163 | }, 164 | 'payslip.employee': { 165 | 'Meta': {'object_name': 'Employee'}, 166 | 'address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 167 | 'company': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'employees'", 'to': "orm['payslip.Company']"}), 168 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 169 | 'hr_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 170 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 171 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 172 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'employees'", 'to': "orm['auth.User']"}) 173 | }, 174 | 'payslip.extrafield': { 175 | 'Meta': {'object_name': 'ExtraField'}, 176 | 'field_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'extra_fields'", 'to': "orm['payslip.ExtraFieldType']"}), 177 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 178 | 'value': ('django.db.models.fields.CharField', [], {'max_length': '200'}) 179 | }, 180 | 'payslip.extrafieldtype': { 181 | 'Meta': {'object_name': 'ExtraFieldType'}, 182 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 183 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 184 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 185 | }, 186 | 'payslip.payment': { 187 | 'Meta': {'object_name': 'Payment'}, 188 | 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '2'}), 189 | 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 1, 5, 0, 0)'}), 190 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 191 | 'employee': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payments'", 'to': "orm['payslip.Employee']"}), 192 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 193 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 194 | 'payment_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payments'", 'to': "orm['payslip.PaymentType']"}) 195 | }, 196 | 'payslip.paymenttype': { 197 | 'Meta': {'object_name': 'PaymentType'}, 198 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 199 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 200 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 201 | } 202 | } 203 | 204 | complete_apps = ['payslip'] -------------------------------------------------------------------------------- /payslip/south_migrations/0002_auto__add_field_employee_is_manager.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | class Migration(SchemaMigration): 10 | 11 | def forwards(self, orm): 12 | # Adding field 'Employee.is_manager' 13 | db.add_column('payslip_employee', 'is_manager', 14 | self.gf('django.db.models.fields.BooleanField')(default=False), 15 | keep_default=False) 16 | 17 | 18 | def backwards(self, orm): 19 | # Deleting field 'Employee.is_manager' 20 | db.delete_column('payslip_employee', 'is_manager') 21 | 22 | 23 | models = { 24 | 'auth.group': { 25 | 'Meta': {'object_name': 'Group'}, 26 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 27 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 28 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 29 | }, 30 | 'auth.permission': { 31 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 32 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 33 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 34 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 35 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 36 | }, 37 | 'auth.user': { 38 | 'Meta': {'object_name': 'User'}, 39 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 40 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 41 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 42 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 43 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 44 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 45 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 46 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 47 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 48 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 49 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 50 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 51 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 52 | }, 53 | 'contenttypes.contenttype': { 54 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 55 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 57 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 58 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 59 | }, 60 | 'payslip.company': { 61 | 'Meta': {'object_name': 'Company'}, 62 | 'address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 63 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 64 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 65 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 66 | }, 67 | 'payslip.employee': { 68 | 'Meta': {'object_name': 'Employee'}, 69 | 'address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 70 | 'company': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'employees'", 'to': "orm['payslip.Company']"}), 71 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 72 | 'hr_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'is_manager': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 75 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 76 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'employees'", 'to': "orm['auth.User']"}) 77 | }, 78 | 'payslip.extrafield': { 79 | 'Meta': {'object_name': 'ExtraField'}, 80 | 'field_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'extra_fields'", 'to': "orm['payslip.ExtraFieldType']"}), 81 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 82 | 'value': ('django.db.models.fields.CharField', [], {'max_length': '200'}) 83 | }, 84 | 'payslip.extrafieldtype': { 85 | 'Meta': {'object_name': 'ExtraFieldType'}, 86 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 87 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 88 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 89 | }, 90 | 'payslip.payment': { 91 | 'Meta': {'object_name': 'Payment'}, 92 | 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '2'}), 93 | 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 1, 6, 0, 0)'}), 94 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 95 | 'employee': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payments'", 'to': "orm['payslip.Employee']"}), 96 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 97 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 98 | 'payment_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payments'", 'to': "orm['payslip.PaymentType']"}) 99 | }, 100 | 'payslip.paymenttype': { 101 | 'Meta': {'object_name': 'PaymentType'}, 102 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 103 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 104 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 105 | } 106 | } 107 | 108 | complete_apps = ['payslip'] -------------------------------------------------------------------------------- /payslip/south_migrations/0003_auto__add_field_extrafieldtype_model__add_field_extrafieldtype_fixed_v.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | class Migration(SchemaMigration): 10 | 11 | def forwards(self, orm): 12 | # Adding field 'ExtraFieldType.model' 13 | db.add_column('payslip_extrafieldtype', 'model', 14 | self.gf('django.db.models.fields.CharField')(max_length=1, null=True, blank=True), 15 | keep_default=False) 16 | 17 | # Adding field 'ExtraFieldType.fixed_values' 18 | db.add_column('payslip_extrafieldtype', 'fixed_values', 19 | self.gf('django.db.models.fields.BooleanField')(default=False), 20 | keep_default=False) 21 | 22 | 23 | def backwards(self, orm): 24 | # Deleting field 'ExtraFieldType.model' 25 | db.delete_column('payslip_extrafieldtype', 'model') 26 | 27 | # Deleting field 'ExtraFieldType.fixed_values' 28 | db.delete_column('payslip_extrafieldtype', 'fixed_values') 29 | 30 | 31 | models = { 32 | 'auth.group': { 33 | 'Meta': {'object_name': 'Group'}, 34 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 35 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 36 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 37 | }, 38 | 'auth.permission': { 39 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 40 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 41 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 42 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 43 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 44 | }, 45 | 'auth.user': { 46 | 'Meta': {'object_name': 'User'}, 47 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 48 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 49 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 50 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 51 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 52 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 53 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 54 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 55 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 56 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 57 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 58 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 59 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 60 | }, 61 | 'contenttypes.contenttype': { 62 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 63 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 64 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 65 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 66 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 67 | }, 68 | 'payslip.company': { 69 | 'Meta': {'object_name': 'Company'}, 70 | 'address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 71 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 72 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 73 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 74 | }, 75 | 'payslip.employee': { 76 | 'Meta': {'object_name': 'Employee'}, 77 | 'address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 78 | 'company': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'employees'", 'to': "orm['payslip.Company']"}), 79 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 80 | 'hr_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 81 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 82 | 'is_manager': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 83 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 84 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'employees'", 'to': "orm['auth.User']"}) 85 | }, 86 | 'payslip.extrafield': { 87 | 'Meta': {'object_name': 'ExtraField'}, 88 | 'field_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'extra_fields'", 'to': "orm['payslip.ExtraFieldType']"}), 89 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 90 | 'value': ('django.db.models.fields.CharField', [], {'max_length': '200'}) 91 | }, 92 | 'payslip.extrafieldtype': { 93 | 'Meta': {'object_name': 'ExtraFieldType'}, 94 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 95 | 'fixed_values': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 96 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 97 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '1', 'null': 'True', 'blank': 'True'}), 98 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 99 | }, 100 | 'payslip.payment': { 101 | 'Meta': {'object_name': 'Payment'}, 102 | 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '2'}), 103 | 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 1, 8, 0, 0)'}), 104 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 105 | 'employee': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payments'", 'to': "orm['payslip.Employee']"}), 106 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 107 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 108 | 'payment_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payments'", 'to': "orm['payslip.PaymentType']"}) 109 | }, 110 | 'payslip.paymenttype': { 111 | 'Meta': {'object_name': 'PaymentType'}, 112 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 113 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 114 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 115 | } 116 | } 117 | 118 | complete_apps = ['payslip'] -------------------------------------------------------------------------------- /payslip/south_migrations/0004_auto__add_field_paymenttype_rrule__chg_field_extrafieldtype_model.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | class Migration(SchemaMigration): 10 | 11 | def forwards(self, orm): 12 | # Adding field 'PaymentType.rrule' 13 | db.add_column(u'payslip_paymenttype', 'rrule', 14 | self.gf('django.db.models.fields.CharField')(default='', max_length=10, blank=True), 15 | keep_default=False) 16 | 17 | 18 | # Changing field 'ExtraFieldType.model' 19 | db.alter_column(u'payslip_extrafieldtype', 'model', self.gf('django.db.models.fields.CharField')(max_length=10, null=True)) 20 | 21 | def backwards(self, orm): 22 | # Deleting field 'PaymentType.rrule' 23 | db.delete_column(u'payslip_paymenttype', 'rrule') 24 | 25 | 26 | # Changing field 'ExtraFieldType.model' 27 | db.alter_column(u'payslip_extrafieldtype', 'model', self.gf('django.db.models.fields.CharField')(max_length=1, null=True)) 28 | 29 | models = { 30 | u'auth.group': { 31 | 'Meta': {'object_name': 'Group'}, 32 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 34 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 35 | }, 36 | u'auth.permission': { 37 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, 38 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 39 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 40 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 41 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 42 | }, 43 | u'auth.user': { 44 | 'Meta': {'object_name': 'User'}, 45 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 47 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 48 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 49 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 50 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 51 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 52 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 53 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 54 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 55 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 56 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 57 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 58 | }, 59 | u'contenttypes.contenttype': { 60 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 61 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 62 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 63 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 64 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 65 | }, 66 | u'payslip.company': { 67 | 'Meta': {'object_name': 'Company'}, 68 | 'address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 69 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 70 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 71 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 72 | }, 73 | u'payslip.employee': { 74 | 'Meta': {'object_name': 'Employee'}, 75 | 'address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 76 | 'company': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'employees'", 'to': u"orm['payslip.Company']"}), 77 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 78 | 'hr_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 79 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 80 | 'is_manager': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 81 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 82 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'employees'", 'to': u"orm['auth.User']"}) 83 | }, 84 | u'payslip.extrafield': { 85 | 'Meta': {'object_name': 'ExtraField'}, 86 | 'field_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'extra_fields'", 'to': u"orm['payslip.ExtraFieldType']"}), 87 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 88 | 'value': ('django.db.models.fields.CharField', [], {'max_length': '200'}) 89 | }, 90 | u'payslip.extrafieldtype': { 91 | 'Meta': {'object_name': 'ExtraFieldType'}, 92 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 93 | 'fixed_values': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 94 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 95 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), 96 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 97 | }, 98 | u'payslip.payment': { 99 | 'Meta': {'object_name': 'Payment'}, 100 | 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '2'}), 101 | 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 5, 7, 0, 0)'}), 102 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 103 | 'employee': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payments'", 'to': u"orm['payslip.Employee']"}), 104 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 105 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 106 | 'payment_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payments'", 'to': u"orm['payslip.PaymentType']"}) 107 | }, 108 | u'payslip.paymenttype': { 109 | 'Meta': {'object_name': 'PaymentType'}, 110 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 111 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 112 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 113 | 'rrule': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'}) 114 | } 115 | } 116 | 117 | complete_apps = ['payslip'] -------------------------------------------------------------------------------- /payslip/south_migrations/0005_auto__add_field_payment_end_date.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | class Migration(SchemaMigration): 10 | 11 | def forwards(self, orm): 12 | # Adding field 'Payment.end_date' 13 | db.add_column(u'payslip_payment', 'end_date', 14 | self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), 15 | keep_default=False) 16 | 17 | 18 | def backwards(self, orm): 19 | # Deleting field 'Payment.end_date' 20 | db.delete_column(u'payslip_payment', 'end_date') 21 | 22 | 23 | models = { 24 | u'auth.group': { 25 | 'Meta': {'object_name': 'Group'}, 26 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 27 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 28 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 29 | }, 30 | u'auth.permission': { 31 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, 32 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 33 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 34 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 35 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 36 | }, 37 | u'auth.user': { 38 | 'Meta': {'object_name': 'User'}, 39 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 40 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 41 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 42 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 43 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 44 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 45 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 46 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 47 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 48 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 49 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 50 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 51 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 52 | }, 53 | u'contenttypes.contenttype': { 54 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 55 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 57 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 58 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 59 | }, 60 | u'payslip.company': { 61 | 'Meta': {'object_name': 'Company'}, 62 | 'address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 63 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 64 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 65 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 66 | }, 67 | u'payslip.employee': { 68 | 'Meta': {'object_name': 'Employee'}, 69 | 'address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 70 | 'company': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'employees'", 'to': u"orm['payslip.Company']"}), 71 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 72 | 'hr_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 73 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'is_manager': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 75 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 76 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'employees'", 'to': u"orm['auth.User']"}) 77 | }, 78 | u'payslip.extrafield': { 79 | 'Meta': {'object_name': 'ExtraField'}, 80 | 'field_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'extra_fields'", 'to': u"orm['payslip.ExtraFieldType']"}), 81 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 82 | 'value': ('django.db.models.fields.CharField', [], {'max_length': '200'}) 83 | }, 84 | u'payslip.extrafieldtype': { 85 | 'Meta': {'object_name': 'ExtraFieldType'}, 86 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 87 | 'fixed_values': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 88 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 89 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), 90 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 91 | }, 92 | u'payslip.payment': { 93 | 'Meta': {'object_name': 'Payment'}, 94 | 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '2'}), 95 | 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 5, 7, 0, 0)'}), 96 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 97 | 'employee': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payments'", 'to': u"orm['payslip.Employee']"}), 98 | 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 99 | 'extra_fields': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['payslip.ExtraField']", 'null': 'True', 'blank': 'True'}), 100 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 101 | 'payment_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payments'", 'to': u"orm['payslip.PaymentType']"}) 102 | }, 103 | u'payslip.paymenttype': { 104 | 'Meta': {'object_name': 'PaymentType'}, 105 | 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 106 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 107 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 108 | 'rrule': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'}) 109 | } 110 | } 111 | 112 | complete_apps = ['payslip'] -------------------------------------------------------------------------------- /payslip/south_migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-payslip/55df83e9cba893f63b9380b23a74784d169972cb/payslip/south_migrations/__init__.py -------------------------------------------------------------------------------- /payslip/static/payslip/css/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 60px; 3 | } -------------------------------------------------------------------------------- /payslip/static/payslip/css/payslip.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | @page { 3 | 4 | -pdf-page-orientation: portrait; 5 | -pdf-page-size: a4; 6 | 7 | margin-top: 2cm; 8 | margin-left: 3cm; 9 | margin-right: 2cm; 10 | margin-bottom: 3cm; 11 | 12 | } 13 | 14 | @page regular { 15 | 16 | -pdf-page-orientation: portrait; 17 | -pdf-page-size: a4; 18 | 19 | margin-top: 2cm; 20 | margin-left: 3cm; 21 | margin-right: 2cm; 22 | margin-bottom: 3cm; 23 | 24 | @frame footer { 25 | -pdf-frame-content: footer; 26 | -pdf-frame-border: 1pt solid black; 27 | bottom: 2cm; 28 | left: 3cm; 29 | right: 2cm; 30 | height: 1cm; 31 | } 32 | } 33 | 34 | html { 35 | border: none; 36 | display: inline; 37 | width: auto; 38 | height: auto; 39 | white-space: normal; 40 | } 41 | 42 | html, p, div, span, h1, h2, table, tr, th, td { 43 | margin: 0; 44 | padding: 0; 45 | } 46 | 47 | address, blockquote, body, center, dl, dir, div, fieldset, form, h1, h2, h3, h4, h5, h6, hr, isindex, menu, noframes, noscript, ol, p, pre, ul, li, dd, dt, pdftoc { 48 | display: block; 49 | } 50 | 51 | #payslipMenu { 52 | display: none; 53 | } 54 | 55 | .tdMiddle { 56 | max-width: 55%; 57 | } 58 | 59 | .tdMini { 60 | max-width: 20%; 61 | } 62 | 63 | .tdSmall { 64 | max-width: 30%; 65 | } 66 | } 67 | 68 | @media screen { 69 | body { 70 | width: 210mm; 71 | margin-top: 55px; 72 | margin-bottom: 100px; 73 | margin-left: auto; 74 | margin-right: auto; 75 | } 76 | 77 | h1 { 78 | width: 115mm; 79 | } 80 | 81 | p { 82 | margin: 0; 83 | } 84 | 85 | #payslipMenu { 86 | position: fixed; 87 | background: #000000; 88 | padding: 5px 0; 89 | width: 100%; 90 | top: 0px; 91 | left: 0px; 92 | height: 27px; 93 | border-bottom: 1px solid #ccc; 94 | box-shadow: 0px 0px 30px #bbb; 95 | } 96 | 97 | #payslipMenu a { 98 | color: #ffffff; 99 | } 100 | 101 | #payslipMenu form { 102 | display: inline; 103 | } 104 | 105 | #payslipMenu form>p { 106 | display: none; 107 | } 108 | 109 | .printButton { 110 | margin-left: 20px; 111 | } 112 | } 113 | 114 | html { 115 | font-weight: normal; 116 | color: #000000; 117 | background-color: transparent; 118 | line-height: 110%; 119 | font-family: Verdana, Tahoma, "Trebuchet MS", "DejuVu Sans", "Bitstream Vera Sans", sans-serif; 120 | font-size: 12px; 121 | } 122 | 123 | #address #addressEmployee, .box .boxContent, #payoutSum, #sum, .altFont { 124 | font-family: "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace; 125 | } 126 | 127 | #address { 128 | width: 57%; 129 | } 130 | 131 | #addressCompany { 132 | font-size: 9px; 133 | padding: 5px 10px; 134 | border: 1px solid #000000; 135 | margin-right: 20px; 136 | } 137 | 138 | #addressEmployee { 139 | padding: 12px; 140 | border-left: 1px solid #000000; 141 | border-right: 1px solid #000000; 142 | border-bottom: 1px solid #000000; 143 | font-weight: bold; 144 | margin-right: 20px; 145 | } 146 | 147 | .box { 148 | border: 1px solid #000000; 149 | padding: 5px 10px 0px; 150 | margin-right: 5px; 151 | } 152 | 153 | .box .boxContent { 154 | white-space: nowrap; 155 | font-size: 80%; 156 | } 157 | 158 | .box .boxHead { 159 | font-size: 70%; 160 | white-space: nowrap; 161 | } 162 | 163 | #employeeExtraFields { 164 | width: 55%; 165 | } 166 | 167 | h1, h2, h3, h4, h5, h6 { 168 | font-weight:bold; 169 | -pdf-outline: true; 170 | -pdf-outline-open: false; 171 | } 172 | 173 | h1 { 174 | font-size:160%; 175 | -pdf-outline-level: 0; 176 | } 177 | 178 | h2 { 179 | font-size: 110%; 180 | margin-top: 50px; 181 | margin-bottom: 10px; 182 | -pdf-outline-level: 1; 183 | padding: 5px; 184 | line-height: 140%; 185 | background-color: #222222; 186 | color: #ffffff; 187 | } 188 | 189 | h3 { 190 | font-size:100%; 191 | -pdf-outline-level: 2; 192 | } 193 | 194 | h4 { 195 | -pdf-outline-level: 3; 196 | } 197 | 198 | h5 { 199 | -pdf-outline-level: 4; 200 | } 201 | 202 | h6 { 203 | -pdf-outline-level: 5; 204 | } 205 | 206 | #payout { 207 | float: left; 208 | } 209 | 210 | #payoutHead { 211 | text-align: right; 212 | padding: 13px 10px 8px; 213 | } 214 | 215 | #payoutCurrency, #payoutSum, #payoutHead { 216 | font-weight: bold; 217 | } 218 | 219 | #payoutCurrency, #payoutSum { 220 | border-top: 3px solid #000000; 221 | border-bottom: 3px solid #000000; 222 | border-left: 1px solid #000000; 223 | border-right: 1px solid #000000; 224 | padding: 10px 10px 5px; 225 | } 226 | 227 | .subHead { 228 | text-align: right; 229 | font-weight: bold; 230 | margin-top: 10px; 231 | margin-bottom: 70px; 232 | } 233 | 234 | .sum { 235 | font-size: 10px; 236 | margin: 5px 9px 5px 0px; 237 | float: left; 238 | } 239 | 240 | .sum strong { 241 | font-size: 14px; 242 | } 243 | 244 | table, thead, tbody, tr { 245 | width: 100%; 246 | } 247 | 248 | table tr { 249 | padding-bottom: 5px; 250 | } 251 | 252 | td, th { 253 | vertical-align: top; 254 | } 255 | 256 | th { 257 | font-size: 90%; 258 | text-align: left; 259 | font-weight: bold; 260 | } -------------------------------------------------------------------------------- /payslip/static/payslip/js/payslip.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('#payslipMenu .printButton').click(function() { 3 | $('#payslipMenu').remove(); 4 | window.print(); 5 | return false; 6 | }); 7 | }); -------------------------------------------------------------------------------- /payslip/templates/payslip/company_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/delete_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block text %}

{% trans "Are you sure that you want to delete this company?" %}

{% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/company_form.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/payslip_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head %} 5 | {% if not form.instance.id %} 6 |

{% trans "Create a new company" %}

7 | {% else %} 8 |

{{ object }}

9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 |
15 |
16 | {% include "django_libs/partials/form.html" with horizontal=1 %} 17 | 18 |
19 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/payslip_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head %}

{% trans "Payslip dashboard" %}

{% endblock %} 5 | 6 | {% block backlink %}{% endblock %} 7 | 8 | {% block content %} 9 | {% trans "Generate payslip" %} 10 |
11 |
12 |
13 |

{% trans "Companies" %}

14 |

{% trans "Host multiple companies or divisions." %}

15 | 16 | 17 | 18 | 19 | 20 | {% for company in companies %} 21 | 22 | 23 | 27 | 28 | {% empty %} 29 | 30 | 31 | 32 | {% endfor %} 33 |
{% trans "Name" %}{% trans "Action" %}
{{ company }} 24 | {% trans "Update" %} 25 | {% trans "Delete" %} 26 |
{% trans "No companies defined." %}
34 | {% trans "Create new company" %} 35 |
36 |
37 |

{% trans "Employees" %}

38 |

{% trans "Add personal data of the employees." %}

39 | 40 | 41 | 42 | 43 | 44 | 45 | {% for employee in employees %} 46 | 47 | 48 | 49 | 53 | 54 | {% empty %} 55 | 56 | 57 | 58 | {% endfor %} 59 |
{% trans "Name" %}{% trans "Company" %}{% trans "Action" %}
{{ employee }}{{ employee.company }} 50 | {% trans "Update" %} 51 | {% trans "Delete" %} 52 |
{% trans "No employees defined." %}
60 | {% trans "Create new employee" %} 61 |
62 |
63 |
64 |
65 |
66 |

{% trans "Payment types" %}

67 |

{% trans "Define single or recurring payment types." %}

68 | 69 | 70 | 71 | 72 | 73 | {% for payment_type in payment_types %} 74 | 75 | 76 | 80 | 81 | {% empty %} 82 | 83 | 84 | 85 | {% endfor %} 86 |
{% trans "Name" %}{% trans "Action" %}
{{ payment_type }} 77 | {% trans "Update" %} 78 | {% trans "Delete" %} 79 |
{% trans "No payment types defined." %}
87 | {% trans "Create new payment type" %} 88 |
89 |
90 |

{% trans "Payments" %}

91 |

{% trans "Add payments to an employee." %}

92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | {% for payment in payments %} 102 | 103 | 104 | 105 | 106 | 107 | 108 | 112 | 113 | {% empty %} 114 | 115 | 116 | 117 | {% endfor %} 118 |
{% trans "Type" %}{% trans "Employee" %}{% trans "Amount" %}{% trans "Date" %}{% trans "End" %}{% trans "Action" %}
{{ payment.payment_type }}{{ payment.employee }}{{ payment.amount }}{{ payment.date|date }}{{ payment.end_date|date }} 109 | {% trans "Update" %} 110 | {% trans "Delete" %} 111 |
{% trans "No payments defined." %}
119 | {% trans "Create new payment" %} 120 |
121 |
122 |
123 |
124 |
125 |

{% trans "Extra fields" %}

126 |

{% trans "Define extra field types for companies, employees and payments." %}

127 | 128 | 129 | 130 | 131 | 132 | {% for type in extra_field_types %} 133 | 134 | 135 | 139 | 140 | {% empty %} 141 | 142 | 143 | 144 | {% endfor %} 145 |
{% trans "Name" %}{% trans "Action" %}
{{ type }} 136 | {% trans "Update" %} 137 | {% trans "Delete" %} 138 |
{% trans "No extra field types defined." %}
146 | {% trans "Create new extra field type" %} 147 |
148 |
149 |

{% trans "Fixed value extra fields" %}

150 |

{% trans "Define fixed values for extra field types." %}

151 | 152 | 153 | 154 | 155 | 156 | {% for field in fixed_value_extra_fields %} 157 | 158 | 159 | 163 | 164 | {% empty %} 165 | 166 | 167 | 168 | {% endfor %} 169 |
{% trans "Name" %}{% trans "Action" %}
{{ field }} 160 | {% trans "Update" %} 161 | {% trans "Delete" %} 162 |
{% trans "No extra fields defined." %}
170 | {% trans "Create new extra field" %} 171 |
172 |
173 |
174 | {% endblock %} 175 | -------------------------------------------------------------------------------- /payslip/templates/payslip/delete_base.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/payslip_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head %}

{{ object }}

{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 | {% include "django_libs/partials/form.html" with horizontal=1 %} 11 | {% block text %}

{% trans "Are you sure that you want to delete this object?" %}

{% endblock %} 12 | 13 |
14 |
15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/employee_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/delete_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block text %}

{% trans "Are you sure that you want to delete this employee?" %}

{% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/employee_form.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/payslip_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head %} 5 | {% if not form.instance.id %} 6 |

{% trans "Create a new employee" %}

7 | {% else %} 8 |

{{ object }}

9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 |
15 |
16 | {% include "django_libs/partials/form.html" with horizontal=1 %} 17 | 18 |
19 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/extrafield_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/delete_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block text %}

{% trans "Are you sure that you want to delete this extra field?" %}

{% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/extrafield_form.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/payslip_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head %} 5 | {% if not form.instance.id %} 6 |

{% trans "Create a new extra field" %}

7 | {% else %} 8 |

{{ object }}

9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 |
15 |
16 | {% include "django_libs/partials/form.html" with horizontal=1 %} 17 | 18 |
19 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/extrafieldtype_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/delete_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block text %}

{% trans "Are you sure that you want to delete this extra field type?" %}

{% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/extrafieldtype_form.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/payslip_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head %} 5 | {% if not form.instance.id %} 6 |

{% trans "Create a new extra field type" %}

7 | {% else %} 8 |

{{ object }}

9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 |
15 |
16 | {% include "django_libs/partials/form.html" with horizontal=1 %} 17 | 18 |
19 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/payment_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/delete_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block text %}

{% trans "Are you sure that you want to delete this payment?" %}

{% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/payment_form.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/payslip_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head %} 5 | {% if not form.instance.id %} 6 |

{% trans "Create a new payment" %}

7 | {% else %} 8 |

{{ object }}

9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 |
15 |
16 | {% include "django_libs/partials/form.html" with horizontal=1 %} 17 | 18 |
19 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/paymenttype_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/delete_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block text %}

{% trans "Are you sure that you want to delete this payment type?" %}

{% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/paymenttype_form.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/payslip_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head %} 5 | {% if not form.instance.id %} 6 |

{% trans "Create a new payment type" %}

7 | {% else %} 8 |

{{ object }}

9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 |
15 |
16 | {% include "django_libs/partials/form.html" with horizontal=1 %} 17 | 18 |
19 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /payslip/templates/payslip/payslip.html: -------------------------------------------------------------------------------- 1 | {% load i18n payslip_tags static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% trans "Payslip" %}: {{ employee }} / {{ date_start }} - {{ date_end }} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 | {% csrf_token %} 28 | {{ form.as_p }} 29 | 30 |
31 | {% trans "Clear payslip" %} 32 |
33 | 34 | 35 | 36 | 37 | 43 | 49 | 55 | 56 | 57 |

{% trans "Payslip" %}

38 |

39 | {% trans "Printed date" %}:
40 | {% now "DATE_FORMAT" %} 41 |

42 |
44 |

45 | {% trans "Period" %}:
46 | {{ date_start|date }} - {{ date_end|date }} 47 |

48 |
50 |

51 | {% trans "HR nr." %}:
52 | {{ employee.hr_number }} 53 |

54 |
58 |

{% trans "Considered as income receipt. Please store it carefully." %}

59 | 60 | 61 | 62 | 70 | 91 | 92 | 93 |
63 |

{{ employee.company }}, {{ employee.company.address }}

64 |

65 | {{ employee.get_title_display }}
66 | {{ employee }}
67 | {{ employee.address|linebreaksbr }} 68 |

69 |
71 | 72 | 73 | {% for field in employee.extra_fields.all %} 74 | {% cycle '' '' '' '' %} 75 | 83 | {% cycle '' '' '' '' %} 84 | {% if forloop.last and not forloop.counter|divisibleby:4 %} 85 | 86 | {% endif %} 87 | {% endfor %} 88 | 89 |
76 | {% if field.value %} 77 |

78 | {{ field.field_type.name }}:
79 | {{ field.value }} 80 |

81 | {% endif %} 82 |
90 |
94 |

{% trans "Earnings / Deductions" %}

95 | {% if payments %} 96 | 97 | 98 | 99 | 100 | {% for field_type in payment_extra_fields %} 101 | 102 | {% endfor %} 103 | 104 | 105 | 106 | 107 | 108 | {% for payment in payments %} 109 | 110 | 111 | {% for field_type in payment_extra_fields %} 112 | 113 | {% endfor %} 114 | 115 | 116 | 117 | {% endfor %} 118 | 119 |
{% trans "Payment type" %}{{ field_type.name }}{% trans "Month" %}{% trans "Amount" %}
{{ payment.payment_type.name }}{{ field_type|get_extra_field_value:payment }}{% if payment.is_recurring %}{{ date_end|date:"M Y" }}{% else %}{{ payment.date|date:"M Y" }}{% endif %}{{ payment.amount|floatformat:2 }}
120 | {% endif %} 121 |

{% trans "Sum earnings" %}: {{ sum|floatformat:2 }}

122 |

{% trans "Sum deductions" %}: {{ sum_neg|floatformat:2 }}

123 |

{% trans "Period sum" %}

124 | 125 | 126 | 127 | 133 | 136 | 139 | 142 | 143 | 144 |
128 |

129 | {% trans "Gross earnings" %}:
130 | {{ sum|floatformat:2 }} 131 |

132 |
134 |

{% trans "Payout" %}:

135 |
137 |

{{ currency }}

138 |
140 |

{{ sum|add:sum_neg|floatformat:2 }}

141 |
145 |

{% blocktrans with date=date_end|date %}Year total (until {{ date }}){% endblocktrans %}

146 | 147 | 148 | 149 | 155 | 161 | 162 | 163 |
150 |

151 | {% trans "Gross total" %}:
152 | {{ sum_year|floatformat:2 }} 153 |

154 |
156 |

157 | {% trans "Net total" %}:
158 | {{ sum_year_neg|floatformat:2 }} 159 |

160 |
164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /payslip/templates/payslip/payslip_base.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | 3 | 4 | 5 | {% block head_title %}Payslip{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% block extrahead %}{% endblock %} 14 | 15 | 16 | 17 | 18 | 38 | 39 | {% block main_container %} 40 |
41 |
42 |
43 | {% block main %} 44 | {% block backlink %}« {% trans "back to dashboard" %}{% endblock %} 45 | {% block head %}{% endblock %} 46 | {% block content %}{% endblock %} 47 | {% endblock %} 48 |
49 |
50 |
51 | {% endblock %} 52 | 53 | -------------------------------------------------------------------------------- /payslip/templates/payslip/payslip_form.html: -------------------------------------------------------------------------------- 1 | {% extends "payslip/payslip_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head %}

{% trans "Generate a payslip" %}

{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 | {% include "django_libs/partials/form.html" with horizontal=1 %} 11 | 12 |
13 |
14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /payslip/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-payslip/55df83e9cba893f63b9380b23a74784d169972cb/payslip/templatetags/__init__.py -------------------------------------------------------------------------------- /payslip/templatetags/payslip_tags.py: -------------------------------------------------------------------------------- 1 | """Variable filters for the ``payslip```application.""" 2 | from django.template import Library 3 | from django.utils.safestring import mark_safe 4 | 5 | from ..models import ExtraField 6 | 7 | register = Library() 8 | 9 | 10 | @register.filter(is_safe=True) 11 | def get_extra_field_value(field_type, payment): 12 | """Returns the value of a specific field type.""" 13 | try: 14 | return payment.extra_fields.get(field_type=field_type).value 15 | except ExtraField.DoesNotExist: 16 | return mark_safe(' ') 17 | -------------------------------------------------------------------------------- /payslip/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-payslip/55df83e9cba893f63b9380b23a74784d169972cb/payslip/tests/__init__.py -------------------------------------------------------------------------------- /payslip/tests/forms_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the forms of the ``payslip`` app.""" 2 | from django.test import TestCase 3 | 4 | from mixer.backend.django import mixer 5 | 6 | from .. import forms 7 | 8 | 9 | class EmployeeFormTestCase(TestCase): 10 | """Tests for the ``EmployeeForm`` model form.""" 11 | longMessage = True 12 | 13 | def test_form(self): 14 | manager = mixer.blend('payslip.Employee', is_manager=True) 15 | data = { 16 | 'first_name': 'Foo', 17 | 'last_name': 'Bar', 18 | 'email': 'test@example.com', 19 | 'password': 'test', 20 | 'retype_password': 'test', 21 | 'title': '1', 22 | } 23 | form = forms.EmployeeForm(company=manager.company, data=data) 24 | self.assertFalse(form.errors) 25 | form.save() 26 | form = forms.EmployeeForm(company=manager.company, data=data) 27 | self.assertFalse(form.is_valid()) 28 | data.update({'password': 'test_fail', 'email': 'test2@example.com'}) 29 | form = forms.EmployeeForm(company=manager.company, data=data) 30 | self.assertFalse(form.is_valid()) 31 | 32 | def test_generate_username(self): 33 | self.user = mixer.blend('auth.User') 34 | self.user.username = forms.generate_username(self.user.email) 35 | self.user.save() 36 | self.assertIsNotNone(forms.generate_username(self.user.email)) 37 | -------------------------------------------------------------------------------- /payslip/tests/models_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the models of the ``payslip`` app.""" 2 | from django.test import TestCase 3 | from django.utils.timezone import now 4 | 5 | from mixer.backend.django import mixer 6 | 7 | 8 | class PaymentTestCase(TestCase): 9 | """Tests for the ``Payment`` model.""" 10 | longMessage = True 11 | 12 | def setUp(self): 13 | self.payment = mixer.blend('payslip.Payment', end_date=now()) 14 | 15 | def test_model(self): 16 | self.assertTrue(str(self.payment)) 17 | 18 | def test_get_end_date_without_tz(self): 19 | self.assertIsNone(self.payment.get_end_date_without_tz().tzinfo, msg=( 20 | 'Should return the end date without timezone attribute')) 21 | -------------------------------------------------------------------------------- /payslip/tests/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | These settings are used by the ``manage.py`` command. 3 | With normal tests we want to use the fastest possible way which is an 4 | in-memory sqlite database but if you want to create South migrations you 5 | need a persistant database. 6 | Unfortunately there seems to be an issue with either South or syncdb so that 7 | defining two routers ("default" and "south") does not work. 8 | """ 9 | from distutils.version import StrictVersion 10 | 11 | import django 12 | 13 | from .test_settings import * # NOQA 14 | 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.sqlite3', 19 | 'NAME': 'db.sqlite', 20 | } 21 | } 22 | 23 | django_version = django.get_version() 24 | if StrictVersion(django_version) < StrictVersion('1.7'): 25 | INSTALLED_APPS.append('south', ) 26 | -------------------------------------------------------------------------------- /payslip/tests/tag_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the template filters and tags of the ``payslip`` app.""" 2 | from django.test import TestCase 3 | 4 | from mixer.backend.django import mixer 5 | 6 | from ..templatetags.payslip_tags import get_extra_field_value 7 | 8 | 9 | class TemplateFilterTestCase(TestCase): 10 | """Tests for the template filters.""" 11 | longMessage = True 12 | 13 | def setUp(self): 14 | self.payment = mixer.blend('payslip.Payment') 15 | self.extra_field = mixer.blend('payslip.ExtraField') 16 | 17 | def test_get_extra_field_value(self): 18 | self.assertEqual(get_extra_field_value( 19 | self.extra_field.field_type, self.payment), ' ') 20 | self.payment.extra_fields.add(self.extra_field) 21 | self.assertEqual(get_extra_field_value( 22 | self.extra_field.field_type, self.payment), self.extra_field.value) 23 | -------------------------------------------------------------------------------- /payslip/tests/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-payslip/55df83e9cba893f63b9380b23a74784d169972cb/payslip/tests/test_app/__init__.py -------------------------------------------------------------------------------- /payslip/tests/test_app/models.py: -------------------------------------------------------------------------------- 1 | # Add models here 2 | -------------------------------------------------------------------------------- /payslip/tests/test_app/templates/404.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-payslip/55df83e9cba893f63b9380b23a74784d169972cb/payslip/tests/test_app/templates/404.html -------------------------------------------------------------------------------- /payslip/tests/test_app/templates/500.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-payslip/55df83e9cba893f63b9380b23a74784d169972cb/payslip/tests/test_app/templates/500.html -------------------------------------------------------------------------------- /payslip/tests/test_settings.py: -------------------------------------------------------------------------------- 1 | """Settings that need to be set in order to run the tests.""" 2 | import os 3 | 4 | 5 | DEBUG = True 6 | USE_TZ = True 7 | 8 | SITE_ID = 1 9 | 10 | DATABASES = { 11 | "default": { 12 | "ENGINE": "django.db.backends.sqlite3", 13 | "NAME": ":memory:", 14 | } 15 | } 16 | 17 | PASSWORD_HASHERS = ( 18 | 'django.contrib.auth.hashers.MD5PasswordHasher', 19 | ) 20 | 21 | ROOT_URLCONF = 'payslip.tests.urls' 22 | 23 | STATIC_URL = '/static/' 24 | 25 | STATIC_ROOT = os.path.join(__file__, '../../static/') 26 | 27 | STATICFILES_DIRS = ( 28 | os.path.join(__file__, 'test_static'), 29 | ) 30 | 31 | MIDDLEWARE_CLASSES = ( 32 | 'django.contrib.sessions.middleware.SessionMiddleware', 33 | 'django.middleware.csrf.CsrfViewMiddleware', 34 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 35 | 'django.contrib.messages.middleware.MessageMiddleware', 36 | 'django.middleware.locale.LocaleMiddleware', 37 | 'django.middleware.common.CommonMiddleware', 38 | ) 39 | 40 | TEMPLATES = [{ 41 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 42 | 'APP_DIRS': True, 43 | 'DIRS': [os.path.join(os.path.dirname(__file__), '../templates')], 44 | 'OPTIONS': { 45 | 'context_processors': ( 46 | 'django.contrib.auth.context_processors.auth', 47 | 'django.template.context_processors.request', 48 | ) 49 | } 50 | }] 51 | 52 | EXTERNAL_APPS = [ 53 | 'django.contrib.admin', 54 | 'django.contrib.admindocs', 55 | 'django.contrib.auth', 56 | 'django.contrib.contenttypes', 57 | 'django.contrib.messages', 58 | 'django.contrib.sessions', 59 | 'django.contrib.staticfiles', 60 | 'django.contrib.sitemaps', 61 | 'django.contrib.sites', 62 | 'django_libs', 63 | ] 64 | 65 | INTERNAL_APPS = [ 66 | 'payslip.tests.test_app', 67 | 'payslip', 68 | ] 69 | 70 | INSTALLED_APPS = EXTERNAL_APPS + INTERNAL_APPS 71 | 72 | SECRET_KEY = 'foobar' 73 | 74 | # Payslip settings 75 | PAYSLIP_CURRENCY = 'SGD' 76 | -------------------------------------------------------------------------------- /payslip/tests/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | This ``urls.py`` is only used when running the tests via ``runtests.py``. 3 | As you know, every app must be hooked into yout main ``urls.py`` so that 4 | you can actually reach the app's views (provided it has any views, of course). 5 | 6 | """ 7 | from django.conf.urls import include, url 8 | from django.contrib import admin 9 | 10 | 11 | admin.autodiscover() 12 | 13 | 14 | urlpatterns = [ 15 | url(r'^admin/', include(admin.site.urls)), 16 | url(r'^', include('payslip.urls')), 17 | ] 18 | -------------------------------------------------------------------------------- /payslip/tests/views_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the views of the ``payslip`` app.""" 2 | from django.test import TestCase 3 | from django.utils import timezone 4 | 5 | from django_libs.tests.mixins import ViewRequestFactoryTestMixin 6 | from mixer.backend.django import mixer 7 | 8 | from .. import views 9 | 10 | 11 | class DashboardViewTestCase(ViewRequestFactoryTestMixin, TestCase): 12 | """Tests for the TemplateView ``DashboardView``.""" 13 | view_class = views.DashboardView 14 | 15 | def setUp(self): 16 | self.user = mixer.blend('auth.User') 17 | 18 | def test_view(self): 19 | self.is_not_callable(user=self.user) 20 | self.user.is_staff = True 21 | self.user.save() 22 | self.is_callable(user=self.user) 23 | 24 | 25 | class CompanyCreateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 26 | """Tests for the CreateView ``CompanyCreateView``.""" 27 | view_class = views.CompanyCreateView 28 | 29 | def setUp(self): 30 | self.user = mixer.blend('auth.User', is_staff=True) 31 | 32 | def test_view(self): 33 | self.is_callable(user=self.user) 34 | self.is_postable(data={'name': 'Foo'}, user=self.user, 35 | to_url_name='payslip_dashboard') 36 | 37 | 38 | class CompanyUpdateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 39 | """Tests for the UpdateView ``CompanyUpdateView``.""" 40 | view_class = views.CompanyUpdateView 41 | 42 | def setUp(self): 43 | self.user = mixer.blend('auth.User') 44 | self.user.is_staff = True 45 | self.user.save() 46 | self.company = mixer.blend('payslip.Company') 47 | 48 | def get_view_kwargs(self): 49 | return {'pk': self.company.pk} 50 | 51 | def test_view(self): 52 | self.is_callable(user=self.user) 53 | 54 | 55 | class CompanyDeleteViewTestCase(ViewRequestFactoryTestMixin, TestCase): 56 | """Tests for the DeleteView ``CompanyDeleteView``.""" 57 | view_class = views.CompanyDeleteView 58 | 59 | def setUp(self): 60 | self.user = mixer.blend('auth.User') 61 | self.company = mixer.blend('payslip.Company') 62 | 63 | def get_view_kwargs(self): 64 | return {'pk': self.company.pk} 65 | 66 | def test_view(self): 67 | self.is_not_callable(user=self.user) 68 | self.user.is_staff = True 69 | self.user.save() 70 | self.is_callable(user=self.user) 71 | self.is_postable(data={'delete': True}, user=self.user, 72 | to_url_name='payslip_dashboard') 73 | 74 | 75 | class EmployeeCreateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 76 | """Tests for the CreateView ``EmployeeCreateView``.""" 77 | view_class = views.EmployeeCreateView 78 | 79 | def setUp(self): 80 | self.manager = mixer.blend('payslip.Employee', is_manager=True) 81 | 82 | def test_view(self): 83 | self.is_callable(user=self.manager.user) 84 | data = { 85 | 'first_name': 'Foo', 86 | 'last_name': 'Bar', 87 | 'email': 'test@example.com', 88 | 'password': 'test', 89 | 'retype_password': 'test', 90 | 'title': '1', 91 | } 92 | self.is_postable(data=data, user=self.manager.user, 93 | to_url_name='payslip_dashboard') 94 | 95 | 96 | class EmployeeUpdateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 97 | """Tests for the UpdateView ``EmployeeUpdateView``.""" 98 | view_class = views.EmployeeUpdateView 99 | 100 | def setUp(self): 101 | self.manager = mixer.blend('payslip.Employee', is_manager=True) 102 | self.employee = mixer.blend('payslip.Employee', 103 | company=self.manager.company) 104 | self.staff = mixer.blend('auth.User', is_staff=True) 105 | extra_field_type = mixer.blend('payslip.ExtraFieldType') 106 | extra_field_type2 = mixer.blend('payslip.ExtraFieldType', 107 | name='Tax Class') 108 | extra_field_type3 = mixer.blend('payslip.ExtraFieldType', 109 | name='Health', fixed_values=False) 110 | mixer.blend('payslip.ExtraFieldType', name='Religion', 111 | fixed_values=False) 112 | extra_field = mixer.blend('payslip.ExtraField', 113 | field_type=extra_field_type) 114 | self.employee.extra_fields.add(extra_field) 115 | extra_field2 = mixer.blend('payslip.ExtraField', 116 | field_type=extra_field_type2, value='II') 117 | self.employee.extra_fields.add(extra_field2) 118 | extra_field3 = mixer.blend('payslip.ExtraField', 119 | field_type=extra_field_type3, value='yes') 120 | self.employee.extra_fields.add(extra_field3) 121 | 122 | def get_view_kwargs(self): 123 | return {'pk': self.employee.pk} 124 | 125 | def test_view(self): 126 | self.is_callable(user=self.manager.user) 127 | self.is_not_callable(user=self.employee.user) 128 | self.is_callable(user=self.staff) 129 | data = { 130 | 'first_name': 'Foo', 131 | 'last_name': 'Bar', 132 | 'email': '{0}'.format(self.employee.user.email), 133 | 'title': '1', 134 | 'Tax Class': 'II', 135 | 'Health': 'no', 136 | 'Religion': 'None', 137 | } 138 | self.is_postable(data=data, user=self.manager.user, 139 | to_url_name='payslip_dashboard') 140 | 141 | 142 | class EmployeeDeleteViewTestCase(ViewRequestFactoryTestMixin, TestCase): 143 | """Tests for the DeleteView ``EmployeeDeleteView``.""" 144 | view_class = views.EmployeeDeleteView 145 | 146 | def setUp(self): 147 | self.manager = mixer.blend('payslip.Employee', is_manager=True) 148 | self.employee = mixer.blend('payslip.Employee', 149 | company=self.manager.company) 150 | 151 | def get_view_kwargs(self): 152 | return {'pk': self.employee.pk} 153 | 154 | def test_view(self): 155 | self.is_callable(user=self.manager.user) 156 | self.is_postable(data={'delete': True}, user=self.manager.user, 157 | to_url_name='payslip_dashboard') 158 | 159 | 160 | class ExtraFieldCreateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 161 | """Tests for the CreateView ``ExtraFieldCreateView``.""" 162 | view_class = views.ExtraFieldCreateView 163 | 164 | def setUp(self): 165 | self.staff = mixer.blend('auth.User', is_staff=True) 166 | self.extra_field_type = mixer.blend('payslip.ExtraFieldType', 167 | fixed_values=True) 168 | 169 | def test_view(self): 170 | self.is_callable(user=self.staff) 171 | data = { 172 | 'field_type': self.extra_field_type.id, 173 | 'value': 'Bar', 174 | } 175 | self.is_postable(data=data, user=self.staff, 176 | to_url_name='payslip_dashboard') 177 | 178 | 179 | class ExtraFieldUpdateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 180 | """Tests for the UpdateView ``ExtraFieldUpdateView``.""" 181 | view_class = views.ExtraFieldUpdateView 182 | 183 | def setUp(self): 184 | self.extra_field = mixer.blend('payslip.ExtraField') 185 | self.staff = mixer.blend('auth.User', is_staff=True) 186 | 187 | def get_view_kwargs(self): 188 | return {'pk': self.extra_field.pk} 189 | 190 | def test_view(self): 191 | self.is_callable(user=self.staff) 192 | 193 | 194 | class ExtraFieldDeleteViewTestCase(ViewRequestFactoryTestMixin, TestCase): 195 | """Tests for the DeleteView ``ExtraFieldDeleteView``.""" 196 | view_class = views.ExtraFieldDeleteView 197 | 198 | def setUp(self): 199 | self.staff = mixer.blend('auth.User', is_staff=True) 200 | self.extra_field = mixer.blend('payslip.ExtraField') 201 | 202 | def get_view_kwargs(self): 203 | return {'pk': self.extra_field.pk} 204 | 205 | def test_view(self): 206 | self.is_callable(user=self.staff) 207 | self.is_postable(data={'delete': True}, user=self.staff, 208 | to_url_name='payslip_dashboard') 209 | 210 | 211 | class ExtraFieldTypeCreateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 212 | """Tests for the CreateView ``ExtraFieldTypeCreateView``.""" 213 | view_class = views.ExtraFieldTypeCreateView 214 | 215 | def setUp(self): 216 | self.staff = mixer.blend('auth.User', is_staff=True) 217 | 218 | def test_view(self): 219 | self.is_callable(user=self.staff) 220 | self.is_postable(data={'name': 'Bar'}, user=self.staff, 221 | to_url_name='payslip_dashboard') 222 | 223 | 224 | class ExtraFieldTypeUpdateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 225 | """Tests for the UpdateView ``ExtraFieldTypeUpdateView``.""" 226 | view_class = views.ExtraFieldTypeUpdateView 227 | 228 | def setUp(self): 229 | self.extra_field_type = mixer.blend('payslip.ExtraFieldType') 230 | self.staff = mixer.blend('auth.User', is_staff=True) 231 | 232 | def get_view_kwargs(self): 233 | return {'pk': self.extra_field_type.pk} 234 | 235 | def test_view(self): 236 | self.is_callable(user=self.staff) 237 | 238 | 239 | class ExtraFieldTypeDeleteViewTestCase(ViewRequestFactoryTestMixin, TestCase): 240 | """Tests for the DeleteView ``ExtraFieldTypeDeleteView``.""" 241 | view_class = views.ExtraFieldTypeDeleteView 242 | 243 | def setUp(self): 244 | self.staff = mixer.blend('auth.User', is_staff=True) 245 | self.extra_field_type = mixer.blend('payslip.ExtraFieldType') 246 | 247 | def get_view_kwargs(self): 248 | return {'pk': self.extra_field_type.pk} 249 | 250 | def test_view(self): 251 | self.is_callable(user=self.staff) 252 | self.is_postable(data={'delete': True}, user=self.staff, 253 | to_url_name='payslip_dashboard') 254 | 255 | 256 | class PaymentCreateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 257 | """Tests for the CreateView ``PaymentCreateView``.""" 258 | view_class = views.PaymentCreateView 259 | 260 | def setUp(self): 261 | self.staff = mixer.blend('auth.User', is_staff=True) 262 | self.payment_type = mixer.blend('payslip.PaymentType') 263 | self.employee = mixer.blend('payslip.Employee') 264 | 265 | def test_view(self): 266 | self.is_callable(user=self.staff) 267 | data = { 268 | 'payment_type': self.payment_type.id, 269 | 'employee': self.employee.id, 270 | 'amount': '1001.00', 271 | 'date': '2013-01-08 09:35:18', 272 | } 273 | self.is_postable(data=data, user=self.staff, 274 | to_url_name='payslip_dashboard') 275 | 276 | 277 | class PaymentUpdateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 278 | """Tests for the UpdateView ``PaymentUpdateView``.""" 279 | view_class = views.PaymentUpdateView 280 | 281 | def setUp(self): 282 | self.staff = mixer.blend('auth.User', is_staff=True) 283 | self.payment = mixer.blend('payslip.Payment') 284 | self.employee = mixer.blend('payslip.Employee') 285 | 286 | def get_view_kwargs(self): 287 | return {'pk': self.payment.pk} 288 | 289 | def test_view(self): 290 | self.is_callable(user=self.staff) 291 | data = { 292 | 'payment_type': self.payment.payment_type.id, 293 | 'employee': self.employee.id, 294 | 'amount': '1001.00', 295 | 'date': '2013-01-08 09:35:18', 296 | } 297 | self.is_postable(data=data, user=self.staff, 298 | to_url_name='payslip_dashboard') 299 | 300 | 301 | class PaymentDeleteViewTestCase(ViewRequestFactoryTestMixin, TestCase): 302 | """Tests for the DeleteView ``PaymentDeleteView``.""" 303 | view_class = views.PaymentDeleteView 304 | 305 | def setUp(self): 306 | self.staff = mixer.blend('auth.User', is_staff=True) 307 | self.payment = mixer.blend('payslip.Payment') 308 | 309 | def get_view_kwargs(self): 310 | return {'pk': self.payment.pk} 311 | 312 | def test_view(self): 313 | self.is_callable(user=self.staff) 314 | self.is_postable(data={'delete': True}, user=self.staff, 315 | to_url_name='payslip_dashboard') 316 | 317 | 318 | class PaymentTypeCreateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 319 | """Tests for the CreateView ``PaymentTypeCreateView``.""" 320 | view_class = views.PaymentTypeCreateView 321 | 322 | def setUp(self): 323 | self.staff = mixer.blend('auth.User', is_staff=True) 324 | 325 | def test_view(self): 326 | self.is_callable(user=self.staff) 327 | self.is_postable(data={'name': 'Bar'}, user=self.staff, 328 | to_url_name='payslip_dashboard') 329 | 330 | 331 | class PaymentTypeUpdateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 332 | """Tests for the UpdateView ``PaymentTypeUpdateView``.""" 333 | view_class = views.PaymentTypeUpdateView 334 | 335 | def setUp(self): 336 | self.staff = mixer.blend('auth.User', is_staff=True) 337 | self.payment_type = mixer.blend('payslip.PaymentType') 338 | 339 | def get_view_kwargs(self): 340 | return {'pk': self.payment_type.pk} 341 | 342 | def test_view(self): 343 | self.is_callable(user=self.staff) 344 | self.is_postable(data={'name': 'Bar'}, user=self.staff, 345 | to_url_name='payslip_dashboard') 346 | 347 | 348 | class PaymentTypeDeleteViewTestCase(ViewRequestFactoryTestMixin, TestCase): 349 | """Tests for the DeleteView ``PaymentTypeDeleteView``.""" 350 | view_class = views.PaymentTypeDeleteView 351 | 352 | def setUp(self): 353 | self.staff = mixer.blend('auth.User', is_staff=True) 354 | self.payment_type = mixer.blend('payslip.PaymentType') 355 | 356 | def get_view_kwargs(self): 357 | return {'pk': self.payment_type.pk} 358 | 359 | def test_view(self): 360 | self.is_callable(user=self.staff) 361 | self.is_postable(data={'delete': True}, user=self.staff, 362 | to_url_name='payslip_dashboard') 363 | 364 | 365 | class PayslipGeneratorViewTestCase(ViewRequestFactoryTestMixin, TestCase): 366 | """Tests for the FormView ``PayslipGeneratorView``.""" 367 | view_class = views.PayslipGeneratorView 368 | 369 | def setUp(self): 370 | self.staff = mixer.blend('auth.User', is_staff=True) 371 | self.manager = mixer.blend('payslip.Employee', is_manager=True) 372 | # Fixtures to test all context functions 373 | self.payment = mixer.blend('payslip.Payment', 374 | payment_type__rrule='MONTHLY') 375 | self.employee = self.payment.employee 376 | self.employee2 = mixer.blend('payslip.Employee', 377 | company=self.manager.company) 378 | mixer.blend('payslip.Payment', payment_type__rrule='MONTHLY', 379 | employee=self.employee, 380 | date=timezone.now() - timezone.timedelta(days=365)) 381 | mixer.blend('payslip.Payment', payment_type__rrule='MONTHLY', 382 | employee=self.employee, amount=-100, 383 | end_date=timezone.now() - timezone.timedelta(days=1)) 384 | 385 | def test_view(self): 386 | self.is_callable(user=self.staff) 387 | data = { 388 | 'employee': self.employee.id, 389 | 'year': timezone.now().year, 390 | 'month': timezone.now().month, 391 | } 392 | self.is_postable(data=data, user=self.staff, ajax=True) 393 | data.update({'employee': self.employee2.id}) 394 | self.is_postable(data=data, user=self.staff, ajax=True) 395 | data.update({'download': True}) 396 | self.is_postable(data=data, user=self.manager.user, ajax=True) 397 | -------------------------------------------------------------------------------- /payslip/urls.py: -------------------------------------------------------------------------------- 1 | """URLs for the ``online_docs`` app.""" 2 | from django.conf.urls import url 3 | 4 | from .views import ( 5 | CompanyCreateView, 6 | CompanyDeleteView, 7 | CompanyUpdateView, 8 | EmployeeCreateView, 9 | EmployeeDeleteView, 10 | EmployeeUpdateView, 11 | ExtraFieldCreateView, 12 | ExtraFieldDeleteView, 13 | ExtraFieldUpdateView, 14 | ExtraFieldTypeCreateView, 15 | ExtraFieldTypeDeleteView, 16 | ExtraFieldTypeUpdateView, 17 | DashboardView, 18 | PaymentCreateView, 19 | PaymentDeleteView, 20 | PaymentUpdateView, 21 | PaymentTypeCreateView, 22 | PaymentTypeDeleteView, 23 | PaymentTypeUpdateView, 24 | PayslipGeneratorView, 25 | ) 26 | 27 | 28 | urlpatterns = [ 29 | url(r'^$', 30 | DashboardView.as_view(), 31 | name='payslip_dashboard', 32 | ), 33 | 34 | url(r'^company/create/$', 35 | CompanyCreateView.as_view(), 36 | name='payslip_company_create', 37 | ), 38 | 39 | url(r'^company/(?P\d+)/update/$', 40 | CompanyUpdateView.as_view(), 41 | name='payslip_company_update', 42 | ), 43 | 44 | url(r'^company/(?P\d+)/delete/$', 45 | CompanyDeleteView.as_view(), 46 | name='payslip_company_delete', 47 | ), 48 | 49 | url(r'^employee/create/$', 50 | EmployeeCreateView.as_view(), 51 | name='payslip_employee_create', 52 | ), 53 | 54 | url(r'^employee/(?P\d+)/update/$', 55 | EmployeeUpdateView.as_view(), 56 | name='payslip_employee_update', 57 | ), 58 | 59 | url(r'^employee/(?P\d+)/delete/$', 60 | EmployeeDeleteView.as_view(), 61 | name='payslip_employee_delete', 62 | ), 63 | 64 | url(r'^extra-field/create/$', 65 | ExtraFieldCreateView.as_view(), 66 | name='payslip_extra_field_create', 67 | ), 68 | 69 | url(r'^extra-field/(?P\d+)/update/$', 70 | ExtraFieldUpdateView.as_view(), 71 | name='payslip_extra_field_update', 72 | ), 73 | 74 | url(r'^extra-field/(?P\d+)/delete/$', 75 | ExtraFieldDeleteView.as_view(), 76 | name='payslip_extra_field_delete', 77 | ), 78 | 79 | url(r'^extra-field-type/create/$', 80 | ExtraFieldTypeCreateView.as_view(), 81 | name='payslip_extra_field_type_create', 82 | ), 83 | 84 | url(r'^extra-field-type/(?P\d+)/update/$', 85 | ExtraFieldTypeUpdateView.as_view(), 86 | name='payslip_extra_field_type_update', 87 | ), 88 | 89 | url(r'^extra-field-type/(?P\d+)/delete/$', 90 | ExtraFieldTypeDeleteView.as_view(), 91 | name='payslip_extra_field_type_delete', 92 | ), 93 | 94 | url(r'^payment/create/$', 95 | PaymentCreateView.as_view(), 96 | name='payslip_payment_create', 97 | ), 98 | 99 | url(r'^payment/(?P\d+)/update/$', 100 | PaymentUpdateView.as_view(), 101 | name='payslip_payment_update', 102 | ), 103 | 104 | url(r'^payment/(?P\d+)/delete/$', 105 | PaymentDeleteView.as_view(), 106 | name='payslip_payment_delete', 107 | ), 108 | 109 | url(r'^payment-type/create/$', 110 | PaymentTypeCreateView.as_view(), 111 | name='payslip_payment_type_create', 112 | ), 113 | 114 | url(r'^payment-type/(?P\d+)/update/$', 115 | PaymentTypeUpdateView.as_view(), 116 | name='payslip_payment_type_update', 117 | ), 118 | 119 | url(r'^payment-type/(?P\d+)/delete/$', 120 | PaymentTypeDeleteView.as_view(), 121 | name='payslip_payment_type_delete', 122 | ), 123 | 124 | url(r'^payslip/$', 125 | PayslipGeneratorView.as_view(), 126 | name='payslip_generator', 127 | ), 128 | ] 129 | -------------------------------------------------------------------------------- /payslip/views.py: -------------------------------------------------------------------------------- 1 | """Views for the ``online_docs`` app.""" 2 | from datetime import datetime 3 | import os 4 | 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 | 18 | from dateutil import relativedelta, rrule 19 | from weasyprint import HTML, CSS 20 | 21 | from .app_settings import CURRENCY 22 | from .forms import ( 23 | EmployeeForm, 24 | ExtraFieldForm, 25 | PaymentForm, 26 | PayslipForm, 27 | ) 28 | from .models import ( 29 | Company, 30 | Employee, 31 | ExtraField, 32 | ExtraFieldType, 33 | Payment, 34 | PaymentType, 35 | ) 36 | 37 | 38 | # -------------# 39 | # Mixins # 40 | # -------------# 41 | 42 | class PermissionMixin(object): 43 | """Mixin to handle security functions.""" 44 | @method_decorator(login_required) 45 | def dispatch(self, request, *args, **kwargs): 46 | """ 47 | Makes sure that the user is logged in and has the right to display this 48 | view. 49 | 50 | """ 51 | if not request.user.is_staff: 52 | raise Http404 53 | return super(PermissionMixin, self).dispatch(request, *args, **kwargs) 54 | 55 | def get_success_url(self): 56 | return reverse('payslip_dashboard') 57 | 58 | 59 | class CompanyMixin(object): 60 | """Mixin to handle company related functions.""" 61 | @method_decorator(login_required) 62 | def dispatch(self, request, *args, **kwargs): 63 | """ 64 | Makes sure that the user is logged in and has the right to display this 65 | view. 66 | 67 | """ 68 | self.kwargs = kwargs 69 | self.object = self.get_object() 70 | try: 71 | Employee.objects.get(company=self.object, user=request.user, 72 | is_manager=True) 73 | except Employee.DoesNotExist: 74 | if not request.user.is_staff: 75 | raise Http404 76 | return super(CompanyMixin, self).dispatch(request, *args, **kwargs) 77 | 78 | def get_success_url(self): 79 | return reverse('payslip_dashboard') 80 | 81 | 82 | class CompanyPermissionMixin(object): 83 | """Mixin to handle company-wide permissions functions.""" 84 | 85 | @method_decorator(login_required) 86 | def dispatch(self, request, *args, **kwargs): 87 | """ 88 | Makes sure that the user is logged in and has the right to display this 89 | view. 90 | 91 | """ 92 | try: 93 | self.company = Employee.objects.get( 94 | user=request.user, is_manager=True).company 95 | except Employee.DoesNotExist: 96 | if not request.user.is_staff: 97 | raise Http404 98 | self.company = None 99 | return super(CompanyPermissionMixin, self).dispatch(request, *args, 100 | **kwargs) 101 | 102 | def get_success_url(self): 103 | return reverse('payslip_dashboard') 104 | 105 | 106 | class EmployeeMixin(object): 107 | """Mixin to handle employee related functions.""" 108 | form_class = EmployeeForm 109 | 110 | def get_form_kwargs(self): 111 | kwargs = super(EmployeeMixin, self).get_form_kwargs() 112 | kwargs.update({'company': self.company}) 113 | return kwargs 114 | 115 | 116 | class ExtraFieldMixin(object): 117 | """Mixin to handle extra field related functions.""" 118 | model = ExtraField 119 | form_class = ExtraFieldForm 120 | 121 | 122 | class ExtraFieldTypeMixin(object): 123 | """Mixin to handle extra field type related functions.""" 124 | model = ExtraFieldType 125 | fields = '__all__' 126 | 127 | 128 | class PaymentMixin(object): 129 | """Mixin to handle payment related functions.""" 130 | model = Payment 131 | form_class = PaymentForm 132 | 133 | 134 | class PaymentTypeMixin(object): 135 | """Mixin to handle payment type related functions.""" 136 | model = PaymentType 137 | fields = '__all__' 138 | 139 | 140 | # -------------# 141 | # Views # 142 | # -------------# 143 | 144 | class DashboardView(PermissionMixin, TemplateView): 145 | """Dashboard to navigate through the payslip app.""" 146 | template_name = 'payslip/dashboard.html' 147 | 148 | def get_context_data(self, **kwargs): 149 | return { 150 | 'companies': Company.objects.all(), 151 | 'employees': Employee.objects.all(), 152 | 'extra_field_types': ExtraFieldType.objects.all(), 153 | 'fixed_value_extra_fields': ExtraField.objects.filter( 154 | field_type__fixed_values=True), 155 | 'payments': Payment.objects.all(), 156 | 'payment_types': PaymentType.objects.all(), 157 | } 158 | 159 | 160 | class CompanyCreateView(PermissionMixin, CreateView): 161 | """Classic view to create a company.""" 162 | model = Company 163 | fields = '__all__' 164 | 165 | def get_success_url(self): 166 | return reverse('payslip_dashboard') 167 | 168 | 169 | class CompanyUpdateView(CompanyMixin, UpdateView): 170 | """Classic view to update a company.""" 171 | model = Company 172 | fields = '__all__' 173 | 174 | 175 | class CompanyDeleteView(CompanyMixin, DeleteView): 176 | """Classic view to delete a company.""" 177 | model = Company 178 | 179 | 180 | class EmployeeCreateView(CompanyPermissionMixin, EmployeeMixin, CreateView): 181 | """Classic view to create an employee.""" 182 | model = Employee 183 | 184 | 185 | class EmployeeUpdateView(CompanyPermissionMixin, EmployeeMixin, UpdateView): 186 | """Classic view to update an employee.""" 187 | model = Employee 188 | 189 | 190 | class EmployeeDeleteView(CompanyPermissionMixin, EmployeeMixin, DeleteView): 191 | """Classic view to delete an employee.""" 192 | model = Employee 193 | 194 | 195 | class ExtraFieldTypeCreateView(PermissionMixin, ExtraFieldTypeMixin, 196 | CreateView): 197 | """Classic view to create an extra field type.""" 198 | pass 199 | 200 | 201 | class ExtraFieldTypeUpdateView(PermissionMixin, ExtraFieldTypeMixin, 202 | UpdateView): 203 | """Classic view to update an extra field type.""" 204 | pass 205 | 206 | 207 | class ExtraFieldTypeDeleteView(PermissionMixin, ExtraFieldTypeMixin, 208 | DeleteView): 209 | """Classic view to delete an extra field type.""" 210 | pass 211 | 212 | 213 | class ExtraFieldCreateView(PermissionMixin, ExtraFieldMixin, CreateView): 214 | """Classic view to create an extra field.""" 215 | pass 216 | 217 | 218 | class ExtraFieldUpdateView(PermissionMixin, ExtraFieldMixin, UpdateView): 219 | """Classic view to update an extra field.""" 220 | pass 221 | 222 | 223 | class ExtraFieldDeleteView(PermissionMixin, ExtraFieldMixin, DeleteView): 224 | """Classic view to delete an extra field.""" 225 | pass 226 | 227 | 228 | class PaymentTypeCreateView(CompanyPermissionMixin, PaymentTypeMixin, 229 | CreateView): 230 | """Classic view to create a payment type.""" 231 | pass 232 | 233 | 234 | class PaymentTypeUpdateView(CompanyPermissionMixin, PaymentTypeMixin, 235 | UpdateView): 236 | """Classic view to update a payment type.""" 237 | pass 238 | 239 | 240 | class PaymentTypeDeleteView(CompanyPermissionMixin, PaymentTypeMixin, 241 | DeleteView): 242 | """Classic view to delete a payment type.""" 243 | pass 244 | 245 | 246 | class PaymentCreateView(CompanyPermissionMixin, PaymentMixin, CreateView): 247 | """Classic view to create a payment.""" 248 | pass 249 | 250 | 251 | class PaymentUpdateView(CompanyPermissionMixin, PaymentMixin, UpdateView): 252 | """Classic view to update a payment.""" 253 | pass 254 | 255 | 256 | class PaymentDeleteView(CompanyPermissionMixin, PaymentMixin, DeleteView): 257 | """Classic view to delete a payment.""" 258 | pass 259 | 260 | 261 | class PayslipGeneratorView(CompanyPermissionMixin, FormView): 262 | """View to present a small form to generate a custom payslip.""" 263 | template_name = 'payslip/payslip_form.html' 264 | form_class = PayslipForm 265 | 266 | def get_form_kwargs(self): 267 | kwargs = super(PayslipGeneratorView, self).get_form_kwargs() 268 | kwargs.update({'company': self.company}) 269 | return kwargs 270 | 271 | def get_template_names(self): 272 | if hasattr(self, 'post_data'): 273 | return ['payslip/payslip.html'] 274 | return super(PayslipGeneratorView, self).get_template_names() 275 | 276 | def get_context_data(self, **kwargs): 277 | kwargs = super(PayslipGeneratorView, self).get_context_data(**kwargs) 278 | if hasattr(self, 'post_data'): 279 | # Get form data 280 | employee = Employee.objects.get(pk=self.post_data.get('employee')) 281 | self.date_start = datetime.strptime( 282 | '{}-{}-01'.format( 283 | self.post_data.get('year'), self.post_data.get('month')), 284 | '%Y-%m-%d', 285 | ) 286 | january_1st = datetime.strptime( 287 | '{}-01-01'.format(self.post_data.get('year')), 288 | '%Y-%m-%d', 289 | ) 290 | date_end = self.date_start + relativedelta.relativedelta( 291 | months=1) - relativedelta.relativedelta(days=1) 292 | 293 | # Get payments for the selected year 294 | payments_year = employee.payments.filter( 295 | # Recurring payments with past date and end_date in the 296 | # selected year or later 297 | Q(date__lte=date_end, end_date__gte=january_1st) | 298 | # Recurring payments with past date in period and open end 299 | Q(date__lte=date_end, end_date__isnull=True, 300 | payment_type__rrule__isnull=False) 301 | ).exclude( 302 | payment_type__rrule__exact='') | employee.payments.filter( 303 | # Single payments in this year 304 | date__year=self.date_start.year, payment_type__rrule__exact='', 305 | ) 306 | 307 | # Get payments for the selected period 308 | payments = payments_year.exclude( 309 | # Exclude single payments not transferred in the period 310 | Q(date__lt=self.date_start) | 311 | Q(date__gt=date_end), 312 | Q(payment_type__rrule__exact=''), 313 | ).filter( 314 | # Recurring payments with past date and end_date in the period 315 | Q(end_date__gte=date_end, date__lte=date_end) | 316 | # Recurring payments with past date in period and open end 317 | Q(date__lte=date_end, end_date__isnull=True) 318 | ) 319 | 320 | # Yearly positive summary 321 | sum_year = payments_year.filter( 322 | amount__gt=0, payment_type__rrule__exact='').aggregate( 323 | Sum('amount')).get('amount__sum') or 0 324 | 325 | # Yearly negative summary 326 | sum_year_neg = payments_year.filter( 327 | amount__lt=0, payment_type__rrule__exact='').aggregate( 328 | Sum('amount')).get('amount__sum') or 0 329 | 330 | # Yearly summary of recurring payments 331 | for payment in payments_year.exclude( 332 | payment_type__rrule__exact=''): 333 | # If the recurring payment started in a year before, let's take 334 | # January 1st as a start, otherwise take the original date 335 | if payment.get_date_without_tz().year < self.date_start.year: 336 | start = january_1st 337 | else: 338 | start = payment.get_date_without_tz() 339 | # If the payments ends before the period's end date, let's take 340 | # this date, otherwise we can take the period's end 341 | if (payment.end_date and 342 | payment.get_end_date_without_tz() < date_end): 343 | end = payment.get_end_date_without_tz() 344 | else: 345 | end = date_end 346 | recurrings = rrule.rrule( 347 | rrule._rrulestr._freq_map.get(payment.payment_type.rrule), 348 | dtstart=start, until=end, 349 | ) 350 | # Multiply amount with recurrings 351 | if payment.amount > 0: 352 | sum_year += payment.amount * recurrings.count() 353 | else: 354 | sum_year_neg += payment.amount * recurrings.count() 355 | 356 | # Period summaries 357 | sum = payments.filter(amount__gt=0).aggregate( 358 | Sum('amount')).get('amount__sum') or 0 359 | sum_neg = payments.filter(amount__lt=0).aggregate( 360 | Sum('amount')).get('amount__sum') or 0 361 | 362 | kwargs.update({ 363 | 'employee': employee, 364 | 'date_start': self.date_start, 365 | 'date_end': date_end, 366 | 'payments': payments, 367 | 'payment_extra_fields': ExtraFieldType.objects.filter( 368 | model='Payment'), 369 | 'sum_year': sum_year, 370 | 'sum_year_neg': sum_year + sum_year_neg, 371 | 'sum': sum, 372 | 'sum_neg': sum_neg, 373 | 'currency': CURRENCY, 374 | }) 375 | return kwargs 376 | 377 | def form_valid(self, form): 378 | self.post_data = self.request.POST 379 | if 'download' in self.post_data: 380 | html = self.render_to_response(self.get_context_data(form=form)) 381 | f = open(os.path.join( 382 | os.path.dirname(__file__), './static/payslip/css/payslip.css')) 383 | html = HTML(string=html.render().content) 384 | pdf = html.write_pdf(stylesheets=[CSS(string=f.read())]) 385 | f.close() 386 | resp = HttpResponse(pdf, content_type='application/pdf') 387 | resp['Content-Disposition'] = \ 388 | u'attachment; filename="{}_{}.pdf"'.format( 389 | self.date_start.year, self.date_start.month) 390 | return resp 391 | return self.render_to_response(self.get_context_data(form=form)) 392 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # Packages essential to this app. Needed by anyone who wants to use this app. 3 | # =========================================================================== 4 | Django 5 | WeasyPrint 6 | django-libs 7 | python-dateutil 8 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This script is used to run tests, create a coverage report and output the 4 | statistics at the end of the tox run. 5 | To run this script just execute ``tox`` 6 | """ 7 | import re 8 | 9 | from fabric.api import local, warn 10 | from fabric.colors import green, red 11 | 12 | 13 | if __name__ == '__main__': 14 | local('flake8 --ignore=E126 --ignore=W391 --statistics' 15 | ' --exclude=submodules,migrations,south_migrations,build .') 16 | local('coverage run --source="payslip" manage.py test -v 2' 17 | ' --traceback --failfast --settings=payslip.tests.settings' 18 | ' --pattern="*_tests.py"') 19 | local('coverage html -d coverage --omit="*__init__*,*/settings/*,' 20 | '*/migrations/*,*/south_migrations/*,*/tests/*,*admin*"') 21 | total_line = local('grep -n pc_cov coverage/index.html', capture=True) 22 | percentage = float(re.findall(r'(\d+)%', total_line)[-1]) 23 | if percentage < 100: 24 | warn(red('Coverage is {0}%'.format(percentage))) 25 | print(green('Coverage is {0}%'.format(percentage))) 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | import payslip 4 | 5 | 6 | def read(fname): 7 | try: 8 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 9 | except IOError: 10 | return '' 11 | 12 | 13 | setup( 14 | name="django-payslip", 15 | version=payslip.__version__, 16 | description=read('DESCRIPTION'), 17 | long_description=read('README.rst'), 18 | license='The MIT License', 19 | platforms=['OS Independent'], 20 | keywords='django, payslip', 21 | author='Tobias Lorenz', 22 | author_email='tobias.lorenz@bitmazk.com', 23 | url="https://github.com/bitmazk/django-payslip", 24 | packages=find_packages(), 25 | include_package_data=True, 26 | install_requires=[ 27 | 'django', 28 | 'weasyprint', 29 | 'django-libs', 30 | 'python-dateutil', 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | mixer 3 | tox 4 | Fabric3 5 | paramiko==1.17.0 6 | ipdb 7 | flake8 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27-django{18,19},py35-django19 3 | 4 | [testenv] 5 | usedevelop = True 6 | deps = 7 | django18: Django>=1.8,<1.9 8 | django19: Django>=1.9,<1.10 9 | -rtest_requirements.txt 10 | commands = python runtests.py 11 | --------------------------------------------------------------------------------