├── account_keeping ├── tests │ ├── __init__.py │ ├── test_app │ │ ├── __init__.py │ │ ├── models.py │ │ └── templates │ │ │ ├── 400.html │ │ │ └── 500.html │ ├── urls.py │ ├── tag_tests.py │ ├── settings.py │ ├── freckle_api_tests.py │ ├── utils_tests.py │ ├── management_tests.py │ ├── test_settings.py │ ├── forms_tests.py │ ├── models_tests.py │ └── views_tests.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── collect_invoices.py │ │ └── importer_mmex.py ├── migrations │ ├── __init__.py │ ├── 0004_branch.py │ ├── 0006_auto_20190102_1114.py │ ├── 0005_auto_20190102_0349.py │ ├── 0008_auto_20190121_1336.py │ ├── 0007_auto_20190121_1334.py │ ├── 0003_auto_20171223_0356.py │ ├── 0001_initial.py │ └── 0002_auto_20161006_1403.py ├── templatetags │ ├── __init__.py │ └── account_keeping_tags.py ├── south_migrations │ ├── __init__.py │ ├── 0002_auto__add_field_invoice_description.py │ ├── 0004_init_invoice_value_fields.py │ ├── 0003_auto__add_field_invoice_value_net__add_field_invoice_value_gross.py │ ├── 0006_auto__del_currencyrate__del_currency.py │ ├── 0005_move_rates_to_history_app.py │ └── 0001_initial.py ├── __init__.py ├── static │ └── account_keeping │ │ ├── css │ │ └── styles.css │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ │ └── js │ │ └── account_keeping.js ├── templates │ └── account_keeping │ │ ├── partials │ │ ├── currency_base_pair.html │ │ ├── transactions_table.html │ │ ├── transactions_table_head.html │ │ ├── invoices_table.html │ │ ├── transactions_table_body.html │ │ └── navigation.html │ │ ├── export.html │ │ ├── payee_form.html │ │ ├── transaction_form.html │ │ ├── invoice_form.html │ │ ├── index_view.html │ │ ├── base.html │ │ ├── payee_list.html │ │ ├── account_list.html │ │ ├── year_view.html │ │ └── accounts_view.html ├── fixtures │ └── account_keeping.json ├── utils.py ├── freckle_api.py ├── admin.py ├── urls.py ├── forms.py └── models.py ├── DESCRIPTION ├── setup.cfg ├── requirements.txt ├── test_requirements.txt ├── AUTHORS ├── .gitignore ├── tox.ini ├── MANIFEST.in ├── .travis.yml ├── manage.py ├── Makefile ├── runtests.py ├── LICENSE ├── hooks └── pre-commit ├── docs └── README.md ├── setup.py ├── CHANGELOG.txt └── README.rst /account_keeping/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account_keeping/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account_keeping/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account_keeping/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account_keeping/tests/test_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account_keeping/tests/test_app/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account_keeping/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account_keeping/south_migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account_keeping/tests/test_app/templates/400.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account_keeping/tests/test_app/templates/500.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account_keeping/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = '0.4.4' 3 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | A reusable Django app for keeping track of transactions in your bank accounts. 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = F999,E501,E128,E124 3 | exclude = .git,*/migrations/*,*/static/CACHE/* 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | django-libs 3 | python-dateutil 4 | django-currency-history 5 | django-import-export 6 | -------------------------------------------------------------------------------- /account_keeping/static/account_keeping/css/styles.css: -------------------------------------------------------------------------------- 1 | body { padding-bottom: 50px; } 2 | .btn-above-table { margin:1em 0px; } 3 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | fabric3 3 | paramiko>=2.0.9 4 | flake8 5 | ipdb 6 | tox 7 | mixer 8 | python-freckle-client 9 | mock 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Current or previous core committers 2 | 3 | Martin Brochhaus 4 | 5 | Contributors (in alphabetical order) 6 | 7 | * Your name could stand here :) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.egg-info/ 3 | *.pyc 4 | *.coverage 5 | *coverage/ 6 | db.sqlite 7 | dist/ 8 | docs/_build/ 9 | app_media/ 10 | app_static/ 11 | .tox/ 12 | -------------------------------------------------------------------------------- /account_keeping/static/account_keeping/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-account-keeping/HEAD/account_keeping/static/account_keeping/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /account_keeping/static/account_keeping/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-account-keeping/HEAD/account_keeping/static/account_keeping/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /account_keeping/static/account_keeping/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-account-keeping/HEAD/account_keeping/static/account_keeping/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/partials/currency_base_pair.html: -------------------------------------------------------------------------------- 1 | {% load account_keeping_tags %} 2 | {{ amount|currency }} 3 | {% if currency.iso_code != base_currency.iso_code %} 4 | ({{ base_amount|currency }} {{ base_currency.iso_code }}) 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include DESCRIPTION 4 | include CHANGELOG.txt 5 | include README.md 6 | graft account_keeping 7 | global-exclude *.orig *.pyc *.log *.swp 8 | prune account_keeping/tests/coverage 9 | prune account_keeping/.ropeproject 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: 5 | - pip install -r test_requirements.txt --use-mirrors 6 | - pip install coveralls 7 | script: 8 | - cd account_keeping/tests 9 | - ./runtests.py 10 | - mv .coverage ../../ 11 | - cd ../../ 12 | after_success: 13 | - coveralls 14 | -------------------------------------------------------------------------------- /account_keeping/tests/urls.py: -------------------------------------------------------------------------------- 1 | """URLs to run the tests.""" 2 | from django.conf.urls import include, url 3 | from django.contrib import admin 4 | 5 | 6 | admin.autodiscover() 7 | 8 | urlpatterns = [ 9 | url(r'^admin/', include(admin.site.urls)), 10 | url(r'^', include('account_keeping.urls')), 11 | ] 12 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/partials/transactions_table.html: -------------------------------------------------------------------------------- 1 | 2 | {% include "account_keeping/partials/transactions_table_head.html" %} 3 | 4 | 5 | {% include "account_keeping/partials/transactions_table_body.html" %} 6 | 7 |
8 | -------------------------------------------------------------------------------- /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 | 'account_keeping.tests.settings') 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | develop: setup-git 2 | pip install "file://`pwd`#egg=account_keeping[dev]" 3 | pip install -e . 4 | pip install -r test_requirements.txt 5 | 6 | setup-git: 7 | git config branch.autosetuprebase always 8 | cd .git/hooks && ln -sf ../../hooks/* ./ 9 | 10 | lint-python: 11 | @echo "Linting Python files" 12 | PYFLAKES_NODOCTEST=1 flake8 account_keeping 13 | @echo "" 14 | -------------------------------------------------------------------------------- /account_keeping/fixtures/account_keeping.json: -------------------------------------------------------------------------------- 1 | [{"pk": 1, "model": "account_keeping.currency", "fields": {"iso_code": "EUR", "name": "Euro"}}, {"pk": 2, "model": "account_keeping.currency", "fields": {"iso_code": "SGD", "name": "Singapore Dollar"}}, {"pk": 1, "model": "account_keeping.account", "fields": {"currency": 1, "total_amount": "0", "initial_amount": "0", "name": "Account One", "slug": "account1"}}, {"pk": 2, "model": "account_keeping.account", "fields": {"currency": 2, "total_amount": "0", "initial_amount": "0", "name": "Account Two", "slug": "account2"}}] -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/partials/transactions_table_head.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | {% trans "ID" %} 5 | {% trans "Date" %} 6 | {% trans "Description" %} 7 | {% trans "Invoice" %} 8 | {% trans "PDF" %} 9 | {% trans "Payee" %} 10 | {% trans "Category" %} 11 | {% trans "Type" %} 12 | {% trans "Amount (net)" %} 13 | {% trans "VAT" %} 14 | {% trans "Amount (gross)" %} 15 | {% if show_balance %}{% trans "Balance (gross)" %}{% endif %} 16 | 17 | 18 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/export.html: -------------------------------------------------------------------------------- 1 | {% extends "account_keeping/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block main %} 5 |
6 |

{% trans "Export" %}

7 |
8 |
9 |
10 | {% include "django_libs/partials/form.html" %} 11 | 12 |
13 |
14 |
15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /account_keeping/tests/tag_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the template tags of the account_keeping app.""" 2 | from django.test import TestCase 3 | 4 | from ..templatetags import account_keeping_tags 5 | 6 | 7 | class CurrencyTestCase(TestCase): 8 | """Tests for the ``currency`` filter.""" 9 | longMessage = True 10 | 11 | def test_tag(self): 12 | self.assertEqual( 13 | account_keeping_tags.currency('1.1111100'), 'EUR 1.11') 14 | 15 | 16 | class GetBranchesTestCase(TestCase): 17 | """Tests for the ``get_branches`` filter.""" 18 | longMessage = True 19 | 20 | def test_tag(self): 21 | self.assertEqual(account_keeping_tags.get_branches().count(), 1) 22 | -------------------------------------------------------------------------------- /account_keeping/static/account_keeping/js/account_keeping.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // Auto-activate tabs 3 | if (location.hash !== '' && $('a[href="' + location.hash + '"]').length) $('a[href="' + location.hash + '"]').tab('show'); 4 | $('a[data-toggle="tab"]').click(function() { 5 | location.hash = $(this).attr('href').substr(1); 6 | }); 7 | 8 | // Show/hide 'mark invoice' field 9 | $('[data-id="invoice-field"]').change(function() { 10 | if ($(this).val()) { 11 | $('[data-id="mark-invoice-field"]').parents('label').show(); 12 | } else { 13 | $('[data-id="mark-invoice-field"]').parents('label').hide(); 14 | } 15 | }).change(); 16 | }); 17 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/payee_form.html: -------------------------------------------------------------------------------- 1 | {% extends "account_keeping/base.html" %} 2 | {% load i18n account_keeping_tags %} 3 | 4 | {% block main %} 5 |
6 |

{% trans "Payee" %}{% if form.instance.pk %} {{ form.instance }}{% endif %}

7 |
8 |
9 |
10 | {% include "django_libs/partials/form.html" %} 11 | 12 |
13 |
14 |
15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/transaction_form.html: -------------------------------------------------------------------------------- 1 | {% extends "account_keeping/base.html" %} 2 | {% load i18n account_keeping_tags %} 3 | 4 | {% block main %} 5 |
6 |

{% trans "Transaction" %}{% if form.instance.pk %} {{ form.instance }}{% endif %}

7 |
8 |
9 |
10 | {% include "django_libs/partials/form.html" %} 11 | 12 |
13 |
14 |
15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /account_keeping/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', ) # NOQA 26 | -------------------------------------------------------------------------------- /account_keeping/migrations/0004_branch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.17 on 2019-01-02 03:37 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('account_keeping', '0003_auto_20171223_0356'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Branch', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=128, verbose_name='Name')), 20 | ('slug', models.SlugField(max_length=128, verbose_name='Slug')), 21 | ], 22 | options={ 23 | 'verbose_name_plural': 'Branches', 24 | 'ordering': ['name'], 25 | 'verbose_name': 'Branch', 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /account_keeping/tests/freckle_api_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the freckle API functions of the account_keeping app.""" 2 | import json 3 | 4 | from django.test import TestCase 5 | 6 | from mixer.backend.django import mixer 7 | from mock import patch 8 | from requests import Response 9 | 10 | from .. import freckle_api 11 | 12 | 13 | class GetUnpaidInvoicesWithTransactionsTestCase(TestCase): 14 | """Tests for the ``get_unpaid_invoices_with_transactions`` function.""" 15 | longMessage = True 16 | 17 | @patch('requests.request') 18 | def test_function(self, mock): 19 | invoice = mixer.blend('account_keeping.Invoice') 20 | mixer.blend('account_keeping.Transaction', invoice=invoice) 21 | resp = Response() 22 | resp.status_code = 200 23 | resp._content = json.dumps([{ 24 | 'reference': invoice.invoice_number, 25 | }]) 26 | mock.return_value = resp 27 | self.assertEqual( 28 | len(freckle_api.get_unpaid_invoices_with_transactions()), 1) 29 | -------------------------------------------------------------------------------- /account_keeping/migrations/0006_auto_20190102_1114.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.17 on 2019-01-02 11:14 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('currency_history', '0001_initial'), 13 | ('account_keeping', '0005_auto_20190102_0349'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='branch', 19 | name='currency', 20 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='branches', to='currency_history.Currency', verbose_name='Currency'), 21 | preserve_default=False, 22 | ), 23 | migrations.AlterField( 24 | model_name='branch', 25 | name='slug', 26 | field=models.SlugField(max_length=128, unique=True, verbose_name='Slug'), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /account_keeping/tests/utils_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the utils of the account_keeping app.""" 2 | import datetime 3 | 4 | from django.test import TestCase 5 | 6 | from .. import utils 7 | 8 | 9 | class GetDateTestCase(TestCase): 10 | """Tests for the ``get_date`` function.""" 11 | longMessage = True 12 | 13 | def test_function(self): 14 | self.assertEqual(utils.get_date('2014-01-01'), 15 | datetime.datetime(2014, 1, 1)) 16 | self.assertEqual(utils.get_date(1), 1) 17 | 18 | 19 | class GetMonthsOfYearTestCase(TestCase): 20 | """Tests for the ``get_months_of_year`` function.""" 21 | longMessage = True 22 | 23 | def test_function(self): 24 | self.assertEqual( 25 | utils.get_months_of_year(datetime.datetime.now().year - 1), 12) 26 | self.assertEqual( 27 | utils.get_months_of_year(datetime.datetime.now().year + 1), 1) 28 | self.assertEqual( 29 | utils.get_months_of_year(datetime.datetime.now().year), 30 | datetime.datetime.now().month) 31 | -------------------------------------------------------------------------------- /account_keeping/utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions for the account_keeping app.""" 2 | from django.utils.timezone import datetime, now 3 | 4 | from six import string_types 5 | 6 | 7 | def get_date(value): 8 | """ 9 | Returns the given field as a DateTime object. 10 | 11 | This is necessary because Postgres and SQLite return different values 12 | for datetime columns (DateTime vs. string). 13 | 14 | """ 15 | if isinstance(value, string_types): 16 | return datetime.strptime(value, '%Y-%m-%d') 17 | return value 18 | 19 | 20 | def get_months_of_year(year): 21 | """ 22 | Returns the number of months that have already passed in the given year. 23 | 24 | This is useful for calculating averages on the year view. For past years, 25 | we should divide by 12, but for the current year, we should divide by 26 | the current month. 27 | 28 | """ 29 | current_year = now().year 30 | if year == current_year: 31 | return now().month 32 | if year > current_year: 33 | return 1 34 | if year < current_year: 35 | return 12 36 | -------------------------------------------------------------------------------- /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,.tox .') 16 | local('coverage run --source="account_keeping" manage.py test -v 2' 17 | ' --traceback --failfast --settings=account_keeping.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 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/invoice_form.html: -------------------------------------------------------------------------------- 1 | {% extends "account_keeping/base.html" %} 2 | {% load i18n account_keeping_tags %} 3 | 4 | {% block main %} 5 |
6 |

{% trans "Invoice" %}{% if form.instance.pk %} {{ form.instance }}{% endif %}

7 |
8 |
9 | {% if last_invoices %} 10 |
11 | {% trans "The last invoice numbers are:" %} 12 |
    13 | {% for no in last_invoices %} 14 |
  • {{ no }}
  • 15 | {% endfor %} 16 |
17 |
18 | {% endif %} 19 |
20 | {% include "django_libs/partials/form.html" %} 21 | 22 |
23 |
24 |
25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014 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 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import glob 4 | import os 5 | import sys 6 | 7 | os.environ['PYFLAKES_NODOCTEST'] = '1' 8 | 9 | # pep8.py uses sys.argv to find setup.cfg 10 | sys.argv = [os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)] 11 | 12 | # git usurbs your bin path for hooks and will always run system python 13 | if 'VIRTUAL_ENV' in os.environ: 14 | site_packages = glob.glob( 15 | '%s/lib/*/site-packages' % os.environ['VIRTUAL_ENV'])[0] 16 | sys.path.insert(0, site_packages) 17 | 18 | 19 | def main(): 20 | from flake8.main import DEFAULT_CONFIG 21 | from flake8.engine import get_style_guide 22 | from flake8.hooks import run 23 | 24 | gitcmd = "git diff-index --cached --name-only HEAD" 25 | 26 | _, files_modified, _ = run(gitcmd) 27 | 28 | # remove non-py files and files which no longer exist 29 | files_modified = filter( 30 | lambda x: x.endswith('.py') and os.path.exists(x), 31 | files_modified) 32 | 33 | flake8_style = get_style_guide(parse_argv=True, config_file=DEFAULT_CONFIG) 34 | report = flake8_style.check_files(files_modified) 35 | 36 | return report.total_errors 37 | 38 | if __name__ == '__main__': 39 | sys.exit(main()) 40 | -------------------------------------------------------------------------------- /account_keeping/templatetags/account_keeping_tags.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | 3 | from django.conf import settings 4 | from django import template 5 | from django.contrib.humanize.templatetags.humanize import intcomma 6 | 7 | from ..models import Branch 8 | 9 | 10 | register = template.Library() 11 | 12 | 13 | @register.simple_tag 14 | def get_branches(): 15 | return Branch.objects.all() 16 | 17 | 18 | @register.filter 19 | def currency(value, currency=None): 20 | try: 21 | currency = currency.iso_code 22 | except AttributeError: 23 | currency = getattr(settings, 'BASE_CURRENCY', 'EUR') 24 | 25 | dec = decimal.Decimal(value) 26 | tup = dec.as_tuple() 27 | delta = len(tup.digits) + tup.exponent 28 | digits = ''.join(str(d) for d in tup.digits) 29 | if delta <= 0: 30 | zeros = abs(tup.exponent) - len(tup.digits) 31 | value = '0.' + ('0' * zeros) + digits 32 | else: 33 | value = digits[:delta] + ('0' * tup.exponent) + '.' + digits[delta:] 34 | value = value.rstrip('0') 35 | if value[-1] == '.': 36 | value = value[:-1] 37 | if not currency or currency != 'BTC': 38 | value = '{0:.2f}'.format(float(value)) 39 | if tup.sign: 40 | value = '-' + value 41 | 42 | return u'{0} {1}'.format(currency, intcomma(value)) 43 | -------------------------------------------------------------------------------- /account_keeping/migrations/0005_auto_20190102_0349.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.17 on 2019-01-02 03:49 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | def get_or_create_branch(apps, schema_editor): 10 | Branch = apps.get_model('account_keeping', 'Branch') 11 | if Branch.objects.count() == 0: 12 | Branch.objects.create(name="Main", slug="main") 13 | 14 | 15 | class Migration(migrations.Migration): 16 | 17 | dependencies = [ 18 | ('account_keeping', '0004_branch'), 19 | ] 20 | 21 | operations = [ 22 | migrations.RunPython(get_or_create_branch), 23 | migrations.AddField( 24 | model_name='account', 25 | name='branch', 26 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to='account_keeping.Branch', verbose_name='Branch'), 27 | preserve_default=False, 28 | ), 29 | migrations.AddField( 30 | model_name='invoice', 31 | name='branch', 32 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='account_keeping.Branch', verbose_name='Branch'), 33 | preserve_default=False, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/index_view.html: -------------------------------------------------------------------------------- 1 | {% extends "account_keeping/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block main %} 5 |
6 |

{% trans "Welcome to Django Account Keeping" %}

7 |

{% trans "Click at one of the main navigation items to start. Soon we will show the most important current stats on this page." %}

8 | 9 | {% if transactions_without_invoice %} 10 |

{% trans "Transactions without invoice" %}

11 | {% include "account_keeping/partials/transactions_table.html" with transactions=transactions_without_invoice %} 12 | {% endif %} 13 | 14 | {% if invoices_without_pdf %} 15 |

{% trans "Invoices without PDF" %}

16 | {% include "account_keeping/partials/invoices_table.html" with invoices=invoices_without_pdf %} 17 | {% endif %} 18 | 19 | {% if unpaid_invoices_with_transactions %} 20 |

{% trans "Unpaid invoices in Freckle" %}

21 | {% if unpaid_invoices_with_transactions.error %} 22 |

{{ unpaid_invoices_with_transactions.error }}

23 | {% elif unpaid_invoices_with_transactions.invoices %} 24 | {% include "account_keeping/partials/invoices_table.html" with invoices=unpaid_invoices_with_transactions %} 25 | {% else %} 26 |

{% trans "Everything is fine :)" %}

27 | {% endif %} 28 | {% endif %} 29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /account_keeping/migrations/0008_auto_20190121_1336.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.17 on 2019-01-21 13:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | def round_amounts(apps, schema_editor): 9 | Invoice = apps.get_model('account_keeping', 'Invoice') 10 | for invoice in Invoice.objects.all(): 11 | invoice.amount_gross = round(invoice.amount_gross, 2) 12 | invoice.amount_net = round(invoice.amount_net, 2) 13 | invoice.value_gross = round(invoice.value_gross, 2) 14 | invoice.value_net = round(invoice.value_net, 2) 15 | Transaction = apps.get_model('account_keeping', 'Transaction') 16 | for transaction in Transaction.objects.all(): 17 | transaction.amount_gross = round(transaction.amount_gross, 2) 18 | transaction.amount_net = round(transaction.amount_net, 2) 19 | transaction.value_gross = round(transaction.value_gross, 2) 20 | transaction.value_net = round(transaction.value_net, 2) 21 | 22 | 23 | class Migration(migrations.Migration): 24 | 25 | dependencies = [ 26 | ('account_keeping', '0007_auto_20190121_1334'), 27 | ] 28 | 29 | operations = [ 30 | migrations.RunPython(round_amounts), 31 | migrations.AlterField( 32 | model_name='invoice', 33 | name='vat', 34 | field=models.DecimalField(decimal_places=0, default=0, max_digits=14, verbose_name='VAT in %'), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /account_keeping/tests/management_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the management commands of the ``account_keeping`` app.""" 2 | from datetime import date 3 | from os import path 4 | 5 | from django.core.management import call_command, CommandError 6 | from django.test import TestCase 7 | 8 | from mixer.backend.django import mixer 9 | 10 | 11 | class CommandTestCase(TestCase): 12 | def test_collect_invoices(self): 13 | transaction = mixer.blend('account_keeping.Transaction', 14 | transaction_date=date.today()) 15 | mixer.blend('account_keeping.Transaction', parent=transaction) 16 | mixer.blend('account_keeping.Transaction', 17 | transaction_date=date.today(), account=transaction.account) 18 | call_command('collect_invoices', account=transaction.account.slug, 19 | start_date=date.today().strftime('%Y-%m-%d'), 20 | end_date=date.today().strftime('%Y-%m-%d'), 21 | output='./foo') 22 | 23 | def test_importer_mmex(self): 24 | currency = mixer.blend('currency_history.Currency') 25 | call_command('importer_mmex', 26 | account=mixer.blend('account_keeping.Account').slug, 27 | currency=currency.iso_code, 28 | filepath=path.abspath(path.join(path.dirname( 29 | path.dirname(__file__)), 'tests', 'test_file.csv'))) 30 | with self.assertRaises(CommandError): 31 | call_command('importer_mmex', currency='FOO') 32 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | Django Account Keeping 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | {% include "account_keeping/partials/navigation.html" %} 23 | {% block main %}{% endblock %} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/payee_list.html: -------------------------------------------------------------------------------- 1 | {% extends "account_keeping/base.html" %} 2 | {% load i18n account_keeping_tags %} 3 | 4 | {% block main %} 5 |
6 |

{% trans "Payees" %}

7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for payee in object_list %} 14 | 15 | 16 | 33 | 36 | 37 | {% endfor %} 38 |
{% trans "Payee" %}{% trans "Invoices" %}{% trans "Transactions" %}
{{ payee }} 17 | 18 |
19 | 20 | 21 | {% for invoice in payee.invoices.all %} 22 | 23 | 26 | 27 | 28 | {% endfor %} 29 | 30 |
24 | {{ invoice }} 25 | {{ invoice.value_net|currency:invoice.currency }}
31 |
32 |
34 | {{ payee.transactions.count }} 35 |
39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/account_list.html: -------------------------------------------------------------------------------- 1 | {% extends "account_keeping/base.html" %} 2 | {% load i18n account_keeping_tags %} 3 | 4 | {% block main %} 5 |
6 |

{% trans "Accounts" %}

7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for account in object_list %} 14 | 15 | 16 | 17 | 36 | 37 | {% endfor %} 38 |
{% trans "Accounts" %}{% trans "Balance" %}{% trans "Transactions" %}
{{ account.name }}{{ account.get_balance|currency:account.currency }} 18 | 19 |
20 | 21 | 22 | {% for transaction in account.transactions.all %} 23 | 24 | 27 | 28 | 29 | 30 | {% endfor %} 31 | 32 |
25 | {{ transaction }} 26 | {{ transaction.transaction_date|date }}{{ transaction.value_net|currency:transaction.currency }}
33 | 34 |
35 |
39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /account_keeping/freckle_api.py: -------------------------------------------------------------------------------- 1 | """API calls against letsfreckle.com""" 2 | from django.conf import settings 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | try: 6 | from freckle_client.client import FreckleClientV2 7 | client = FreckleClientV2(settings.ACCOUNT_KEEPING_FRECKLE_ACCESS_TOKEN) 8 | except ImportError: # pragma: nocover 9 | client = None 10 | from requests.exceptions import ConnectionError, HTTPError 11 | 12 | from . import models 13 | 14 | 15 | def get_unpaid_invoices_with_transactions(branch=None): 16 | """ 17 | Returns all invoices that are unpaid on freckle but have transactions. 18 | 19 | This means, that the invoice is either partially paid and can be left as 20 | unpaid in freckle, or the invoice has been fully paid and should be set to 21 | paid in freckle as well. 22 | 23 | """ 24 | if not client: # pragma: nocover 25 | return None 26 | result = {} 27 | try: 28 | unpaid_invoices = client.fetch_json( 29 | 'invoices', query_params={'state': 'unpaid'}) 30 | except (ConnectionError, HTTPError): # pragma: nocover 31 | result.update({'error': _('Wasn\'t able to connect to Freckle.')}) 32 | else: 33 | invoices = [] 34 | for invoice in unpaid_invoices: 35 | invoice_with_transactions = models.Invoice.objects.filter( 36 | invoice_number=invoice['reference'], 37 | transactions__isnull=False) 38 | if branch: 39 | invoice_with_transactions = invoice_with_transactions.filter( 40 | branch=branch) 41 | if invoice_with_transactions: 42 | invoices.append(invoice) 43 | result.update({'invoices': invoices}) 44 | return result 45 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # How to create your Sphinx documentation 2 | 3 | In order to kickstart your Sphinx documentation, please do the following: 4 | 5 | ## Create virtual environment. 6 | 7 | If you haven't done so already, create a virtual environment for this reusable 8 | app like so: 9 | 10 | mkvirtualenv -p python2.7 django-account-keeping 11 | pip install Sphinx 12 | deactivate 13 | workon django-account-keeping 14 | sphinx-quickstart 15 | 16 | Answer the questions: 17 | 18 | > Root path for the documentation [.]: 19 | > Separate source and build directories (y/N) [n]: y 20 | > Name prefix for templates and static dir [_]: 21 | > Project name: Django Account Keeping 22 | > Author name(s): Martin Brochhaus 23 | > Project version: 0.1 24 | > Project release [0.1]: 25 | > Source file suffix [.rst]: 26 | > Name of your master document (without suffix) [index]: 27 | > Do you want to use the epub builder (y/N) [n]: 28 | > autodoc: automatically insert docstrings from modules (y/N) [n]: y 29 | > doctest: automatically test code snippets in doctest blocks (y/N) [n]: 30 | > intersphinx: link between Sphinx documentation of different projects (y/N) [n]: y 31 | > todo: write "todo" entries that can be shown or hidden on build (y/N) [n]: y 32 | > coverage: checks for documentation coverage (y/N) [n]: y 33 | > pngmath: include math, rendered as PNG images (y/N) [n]: 34 | > mathjax: include math, rendered in the browser by MathJax (y/N) [n]: 35 | > ifconfig: conditional inclusion of content based on config values (y/N) [n]: y 36 | > viewcode: include links to the source code of documented Python objects (y/N) [n]: y 37 | > Create Makefile? (Y/n) [y]: 38 | > Create Windows command file? (Y/n) [y]: 39 | -------------------------------------------------------------------------------- /account_keeping/admin.py: -------------------------------------------------------------------------------- 1 | """Admin classes for the account_keeping app.""" 2 | from django.contrib import admin 3 | 4 | from . import models 5 | 6 | 7 | class BranchAdmin(admin.ModelAdmin): 8 | list_display = ['name', 'slug'] 9 | admin.site.register(models.Branch, BranchAdmin) 10 | 11 | 12 | class AccountAdmin(admin.ModelAdmin): 13 | list_display = [ 14 | 'name', 'slug', 'currency', 'initial_amount', 'total_amount', 'branch'] 15 | list_filter = ['branch'] 16 | admin.site.register(models.Account, AccountAdmin) 17 | 18 | 19 | class InvoiceAdmin(admin.ModelAdmin): 20 | list_display = [ 21 | 'invoice_type', 'invoice_date', 'invoice_number', 'description', 22 | 'currency', 'amount_net', 'vat', 23 | 'amount_gross', 'payment_date', 'branch'] 24 | list_filter = ['invoice_type', 'currency', 'payment_date', 'branch'] 25 | date_hierarchy = 'invoice_date' 26 | search_fields = ['invoice_number', 'description'] 27 | admin.site.register(models.Invoice, InvoiceAdmin) 28 | 29 | 30 | class PayeeAdmin(admin.ModelAdmin): 31 | list_display = ['name', ] 32 | admin.site.register(models.Payee, PayeeAdmin) 33 | 34 | 35 | class CategoryAdmin(admin.ModelAdmin): 36 | list_display = ['name', ] 37 | admin.site.register(models.Category, CategoryAdmin) 38 | 39 | 40 | class TransactionAdmin(admin.ModelAdmin): 41 | list_display = [ 42 | 'transaction_date', 'parent', 'invoice_number', 'invoice', 'payee', 43 | 'description', 'category', 'currency', 'value_net', 'vat', 44 | 'value_gross', ] 45 | list_filter = ['account', 'currency', 'payee', 'category'] 46 | date_hierarchy = 'transaction_date' 47 | raw_id_fields = ['parent', 'invoice'] 48 | search_fields = [ 49 | 'invoice_number', 'invoice__invoice_number', 'description'] 50 | admin.site.register(models.Transaction, TransactionAdmin) 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Python setup file for the account_keeping app. 4 | 5 | In order to register your app at pypi.python.org, create an account at 6 | pypi.python.org and login, then register your new app like so: 7 | 8 | python setup.py register 9 | 10 | If your name is still free, you can now make your first release but first you 11 | should check if you are uploading the correct files: 12 | 13 | python setup.py sdist 14 | 15 | Inspect the output thoroughly. There shouldn't be any temp files and if your 16 | app includes staticfiles or templates, make sure that they appear in the list. 17 | If something is wrong, you need to edit MANIFEST.in and run the command again. 18 | 19 | If all looks good, you can make your first release: 20 | 21 | python setup.py sdist upload 22 | 23 | For new releases, you need to bump the version number in 24 | account_keeping/__init__.py and re-run the above command. 25 | 26 | For more information on creating source distributions, see 27 | http://docs.python.org/2/distutils/sourcedist.html 28 | 29 | """ 30 | import os 31 | from setuptools import setup, find_packages 32 | import account_keeping as app 33 | 34 | 35 | install_requires = [ 36 | 'django', 37 | 'django-libs>=1.61.1', 38 | 'python-dateutil', 39 | 'django-currency-history', 40 | 'django-import-export', 41 | ] 42 | 43 | 44 | def read(fname): 45 | try: 46 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 47 | except IOError: 48 | return '' 49 | 50 | setup( 51 | name="django-account-keeping", 52 | version=app.__version__, 53 | description=read('DESCRIPTION'), 54 | long_description=read('README.rst'), 55 | license='The MIT License', 56 | platforms=['OS Independent'], 57 | keywords='django, app, reusable, accounting, finance, banking', 58 | author='Martin Brochhaus', 59 | author_email='mbrochh@gmail.com', 60 | url="https://github.com/bitmazk/django-account-keeping", 61 | packages=find_packages(), 62 | include_package_data=True, 63 | install_requires=install_requires, 64 | ) 65 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/partials/invoices_table.html: -------------------------------------------------------------------------------- 1 | {% load account_keeping_tags i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% for invoice in invoices %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 39 | 40 | {% endfor %} 41 | 42 |
{% trans "ID" %}{% trans "Date" %}{% trans "Invoice number" %}{% trans "PDF" %}{% trans "Description" %}{% trans "Currency" %}{% trans "Amount (net)" %}{% trans "VAT" %}{% trans "Amount (gross)" %}{% trans "Balance" %}{% trans "Transactions" %}
{{ invoice.pk }}{{ invoice.invoice_date|date:"Y-m-d" }}{{ invoice.invoice_number|default:"n/a" }}{% if invoice.pdf %}{% else %}n/a{% endif %}{{ invoice.description|default:"n/a" }}{{ invoice.currency.iso_code }}{{ invoice.amount_net|currency:invoice.currency }}{{ invoice.vat|currency:invoice.currency }}{{ invoice.amount_gross|currency:invoice.currency }}{{ invoice.balance|currency:invoice.currency }} 32 | {% for transaction in invoice.transactions.all %} 33 | {{ transaction.pk }}{% if not forloop.last %}, {% endif %} 34 | {% endfor %} 35 | 37 | {% trans "Add transaction" %} 38 |
43 | -------------------------------------------------------------------------------- /account_keeping/urls.py: -------------------------------------------------------------------------------- 1 | """URLs for the account_keeping app.""" 2 | from django.conf.urls import url 3 | 4 | from . import views 5 | 6 | 7 | urlpatterns = [ 8 | url(r'transaction/(?P\d+)/$', 9 | views.TransactionUpdateView.as_view(), 10 | name='account_keeping_transaction_update'), 11 | 12 | url(r'transaction/create/$', 13 | views.TransactionCreateView.as_view(), 14 | name='account_keeping_transaction_create'), 15 | 16 | url(r'invoice/(?P\d+)/$', 17 | views.InvoiceUpdateView.as_view(), 18 | name='account_keeping_invoice_update'), 19 | 20 | url(r'invoice/create/$', 21 | views.InvoiceCreateView.as_view(), 22 | name='account_keeping_invoice_create'), 23 | 24 | url(r'accounts/$', 25 | views.AccountListView.as_view(), 26 | name='account_keeping_accounts'), 27 | 28 | url(r'payees/(?P\d+)/$', 29 | views.PayeeUpdateView.as_view(), 30 | name='account_keeping_payee_update'), 31 | 32 | url(r'payees/create/$', 33 | views.PayeeCreateView.as_view(), 34 | name='account_keeping_payee_create'), 35 | 36 | url(r'payees/$', 37 | views.PayeeListView.as_view(), 38 | name='account_keeping_payees'), 39 | 40 | url(r'export/$', 41 | views.TransactionExportView.as_view(), 42 | name='account_keeping_export'), 43 | 44 | url(r'all/$', 45 | views.AllTimeView.as_view(), 46 | name='account_keeping_all'), 47 | 48 | url(r'(?P\d+)/(?P\d+)/$', 49 | views.MonthView.as_view(), 50 | name='account_keeping_month'), 51 | 52 | url(r'(?P\d+)/$', 53 | views.YearOverviewView.as_view(), 54 | name='account_keeping_year'), 55 | 56 | url(r'current-year/$', 57 | views.CurrentYearRedirectView.as_view(), 58 | name='account_keeping_current_year'), 59 | 60 | url(r'current-month/$', 61 | views.CurrentMonthRedirectView.as_view(), 62 | name='account_keeping_current_month'), 63 | 64 | url(r'branch/(?P[\w-]+)/activate/$', 65 | views.BranchSelectView.as_view(), 66 | name='account_keeping_select_branch'), 67 | 68 | url(r'$', 69 | views.IndexView.as_view(), 70 | name='account_keeping_index'), 71 | ] 72 | -------------------------------------------------------------------------------- /account_keeping/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 | SITE_ID = 1 7 | 8 | APP_ROOT = os.path.abspath( 9 | os.path.join(os.path.dirname(__file__), '..')) 10 | 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', 15 | 'NAME': ':memory:', 16 | } 17 | } 18 | 19 | ROOT_URLCONF = 'account_keeping.tests.urls' 20 | 21 | STATIC_URL = '/static/' 22 | STATIC_ROOT = os.path.join(APP_ROOT, '../app_static') 23 | MEDIA_ROOT = os.path.join(APP_ROOT, '../app_media') 24 | STATICFILES_DIRS = ( 25 | os.path.join(APP_ROOT, 'static'), 26 | ) 27 | 28 | TEMPLATES = [{ 29 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 30 | 'APP_DIRS': True, 31 | 'DIRS': [os.path.join(APP_ROOT, 'tests/test_app/templates')], 32 | 'OPTIONS': { 33 | 'context_processors': ( 34 | 'django.contrib.auth.context_processors.auth', 35 | 'django.template.context_processors.i18n', 36 | 'django.template.context_processors.request', 37 | ) 38 | } 39 | }] 40 | 41 | MIDDLEWARE_CLASSES = [ 42 | 'django.contrib.sessions.middleware.SessionMiddleware', 43 | 'django.middleware.common.CommonMiddleware', 44 | 'django.middleware.csrf.CsrfViewMiddleware', 45 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 46 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 47 | 'django.contrib.messages.middleware.MessageMiddleware', 48 | ] 49 | 50 | EXTERNAL_APPS = [ 51 | 'django.contrib.admin', 52 | 'django.contrib.admindocs', 53 | 'django.contrib.auth', 54 | 'django.contrib.contenttypes', 55 | 'django.contrib.messages', 56 | 'django.contrib.sessions', 57 | 'django.contrib.staticfiles', 58 | 'django.contrib.sitemaps', 59 | 'django.contrib.sites', 60 | 'django.contrib.humanize', 61 | 'django_libs', 62 | 'currency_history', 63 | 'import_export', 64 | ] 65 | 66 | INTERNAL_APPS = [ 67 | 'account_keeping', 68 | 'account_keeping.tests.test_app', 69 | ] 70 | 71 | INSTALLED_APPS = EXTERNAL_APPS + INTERNAL_APPS 72 | 73 | SECRET_KEY = 'foobar' 74 | 75 | CURRENCY_SERVICE = 'yahoo' 76 | 77 | # Freckle 78 | ACCOUNT_KEEPING_FRECKLE_ACCESS_TOKEN = 'Foo' 79 | -------------------------------------------------------------------------------- /account_keeping/tests/forms_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the forms of the account_keeping app.""" 2 | from django.test import TestCase 3 | from django.utils.timezone import now 4 | 5 | from mixer.backend.django import mixer 6 | 7 | from .. import forms 8 | 9 | 10 | class InvoiceFormTestCase(TestCase): 11 | """Tests for the ``InvoiceForm`` form.""" 12 | longMessage = True 13 | 14 | def test_form(self): 15 | form = forms.InvoiceForm(branch=None, data={ 16 | 'invoice_type': 'd', 17 | 'invoice_date': now().strftime('%Y-%m-%d'), 18 | 'currency': mixer.blend('currency_history.Currency').pk, 19 | 'branch': mixer.blend('account_keeping.Branch').pk, 20 | 'vat': 0, 21 | 'value_net': 0, 22 | 'value_gross': 0, 23 | }) 24 | self.assertFalse(form.errors) 25 | 26 | 27 | class TransactionFormTestCase(TestCase): 28 | """Tests for the ``TransactionForm`` form.""" 29 | longMessage = True 30 | 31 | def test_form(self): 32 | account = mixer.blend('account_keeping.Account') 33 | data = { 34 | 'transaction_type': 'd', 35 | 'transaction_date': now().strftime('%Y-%m-%d'), 36 | 'account': account.pk, 37 | 'payee': mixer.blend('account_keeping.Payee').pk, 38 | 'category': mixer.blend('account_keeping.Category').pk, 39 | 'currency': mixer.blend('currency_history.Currency').pk, 40 | 'amount_net': 0, 41 | 'amount_gross': 0, 42 | 'vat': 0, 43 | 'value_net': 0, 44 | 'value_gross': 0, 45 | 'mark_invoice': True, 46 | } 47 | form = forms.TransactionForm(data=data, branch=account.branch) 48 | self.assertFalse(form.errors) 49 | transaction = form.save() 50 | transaction.invoice = mixer.blend('account_keeping.Invoice', 51 | payment_date=None) 52 | transaction.invoice.save() 53 | self.assertFalse(transaction.invoice.payment_date) 54 | data.update({'invoice': transaction.invoice.pk}) 55 | form = forms.TransactionForm(data=data, branch=account.branch) 56 | self.assertFalse(form.errors) 57 | transaction = form.save() 58 | self.assertTrue(transaction.invoice.payment_date) 59 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/partials/transactions_table_body.html: -------------------------------------------------------------------------------- 1 | {% load account_keeping_tags humanize i18n libs_tags %} 2 | {% for transaction in transactions %} 3 | 4 | {{ transaction.pk }} 5 | {{ transaction.transaction_date|date:"Y-m-d" }} 6 | {{ transaction.get_description|linebreaksbr }} 7 | 8 | {% for invoice in transaction.get_invoices %} 9 | {% if invoice %} 10 | {{ invoice.invoice_number|default:invoice.pk }} 11 | {% elif transaction.invoice_number %} 12 | {{ transaction.invoice_number }} 13 | {% else %} 14 | {% trans "n/a" %} 15 | {% endif %} 16 | {% if not forloop.last %}, {% endif %} 17 | {% endfor %} 18 | 19 | 20 | {% for invoice in transaction.get_invoices %} 21 | {% if invoice.pdf %}{% else %}n/a{% endif %} 22 | {% endfor %} 23 | 24 | {{ transaction.payee }} 25 | {{ transaction.category }} 26 | {% if transaction.transaction_type == 'w' %}{% trans "DR" %}{% else %}{% trans "CR" %}{% endif %} 27 | {{ transaction.amount_net|currency:transaction.currency }} 28 | {{ transaction.vat|currency:transaction.currency }} 29 | {{ transaction.amount_gross|currency:transaction.currency }} 30 | {% if show_balance %} 31 | {{ BALANCE|currency:transaction.currency }} 32 | {% sum "BALANCE" transaction.value_gross multiplier=-1 %} 33 | {% endif %} 34 | 35 | {% trans "Add transaction" %} 36 | 37 | 38 | {% endfor %} 39 | -------------------------------------------------------------------------------- /account_keeping/management/commands/collect_invoices.py: -------------------------------------------------------------------------------- 1 | """ 2 | Collects invoices for the given account and timeframe into a tarball. 3 | 4 | """ 5 | import datetime 6 | import os 7 | import shutil 8 | 9 | from django.core.management.base import BaseCommand 10 | 11 | from ... import models 12 | 13 | 14 | class Command(BaseCommand): 15 | help = 'Copies invoices of transactions into a folder.' 16 | 17 | def add_arguments(self, parser): 18 | parser.add_argument( 19 | '-o', '--output', 20 | dest='output', 21 | help='Output folder. Make sure that the folder exists.', 22 | ) 23 | parser.add_argument( 24 | '-a', '--account', 25 | dest='account', 26 | help='Account slug of the account that should be handled.', 27 | ) 28 | parser.add_argument( 29 | '-s', '--start', 30 | dest='start_date', 31 | help='Start date. Include all transactions from this date.', 32 | ) 33 | parser.add_argument( 34 | '-e', '--end', 35 | dest='end_date', 36 | help='End date. Include all transactions up to this date.', 37 | ) 38 | 39 | def __init__(self, *args, **kwargs): 40 | super(Command, self).__init__(*args, **kwargs) 41 | self.counter = 1 42 | 43 | def copy_file(self, transaction): 44 | if transaction.invoice and transaction.invoice.pdf: # pragma: nocover 45 | counter = str(self.counter).zfill(4) 46 | filename = '{0}.pdf'.format(counter) 47 | shutil.copy( 48 | transaction.invoice.pdf.file.name, 49 | os.path.join(self.output_folder, filename)) 50 | self.counter += 1 51 | 52 | def handle(self, *args, **options): 53 | account = models.Account.objects.get(slug=options.get('account')) 54 | self.output_folder = options.get('output') 55 | start_date = datetime.datetime.strptime( 56 | options.get('start_date'), '%Y-%m-%d') 57 | end_date = datetime.datetime.strptime( 58 | options.get('end_date'), '%Y-%m-%d') 59 | transactions = models.Transaction.objects.filter( 60 | account=account, 61 | transaction_date__gte=start_date, 62 | transaction_date__lte=end_date, 63 | ).prefetch_related('children', ).order_by('-transaction_date') 64 | for transaction in transactions: 65 | if transaction.children.all(): 66 | for child in transaction.children.all(): 67 | self.copy_file(child) 68 | else: 69 | self.copy_file(transaction) 70 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | === ongoing (0.4.X - to be released as 0.5) === 2 | 3 | - Assigned currencies to branches 4 | - Fixed invoice number bug 5 | - Added more fields to admin lists 6 | - Add round for invoice and transaction amount calculation 7 | 8 | === 0.4 === 9 | 10 | - Allow up to 10 decimal places 11 | - Substract partial payments from outstanding amounts 12 | - Added branches 13 | 14 | === 0.3 === 15 | 16 | - Added active field to account 17 | - Hide inactive accounts and remove them from yearly overview 18 | - Added transaction export view 19 | - Fixed freckle integration 20 | - Added payee management views 21 | - Improved transaction form 22 | - Added enctype and novalidate parameters to forms 23 | - Added letsfreckle invoices to dashboard 24 | - Improved and enhanced transaction form 25 | - Added account statistics 26 | - Added CU views for transactions and invoices 27 | - Improved balance to handle multi-currency objects 28 | - Fixed payee template 29 | - Replaced locale currency with intcomma and floatformat 30 | - Added payee overview and invoice balance 31 | - Prepared app for Django 1.9 and Python 3.5 32 | - Bugfix: Fall back to old currency histories if there's no current one 33 | - Bugfix: Home view crashes when account is empty 34 | - Bugfix: Fixed Unicode bug in `Transaction.get_description` 35 | - Bugfix: Income column in year view did not convert foreign currency's before adding 36 | - Added `Type` column to tansactions table 37 | - Added `collect_invoices` management command 38 | 39 | === 0.2 === 40 | 41 | - Added migration to move currency data 42 | - Added django-currency-history to auto-track rates 43 | - Removed currency and currency rate model 44 | - Better rendering of description and invoice PDF, considering children 45 | - Bugfix: next_month calculation was faulty 46 | - Bugfix: Making timezone aware row values naive 47 | - Showing whole year on year view, even future months 48 | - Added description column to invoice 49 | - Added /current-month/ and /current-year/ views 50 | - Added filters to admins 51 | - Bugfix: Filter for outstanding invoices was wrong 52 | - Added unicode method to CurrencyRate model 53 | - Rendering months with localtime off on year_view.html 54 | - sqlite and postgres return different things when truncating a date, applied 55 | quick fix so that it works with postgres / need to find a workaround so that 56 | tests and real-world-usage both work 57 | - added 'parent' to Transaction list admin 58 | 59 | === 0.1 === 60 | - Initial commit 61 | 62 | 63 | # Suggested file syntax: 64 | # 65 | # === (ongoing) === 66 | # - this is always on top of the file 67 | # - when you release a new version, you rename the last `(ongoing)` to the new 68 | # version and add a new `=== (ongoing) ===` to the top of the file 69 | # 70 | # === 1.0 === 71 | # - a major version is created when the software reached a milestone and is 72 | # feature complete 73 | # 74 | # === 0.2 === 75 | # - a minor version is created when new features or significant changes have 76 | # been made to the software. 77 | # 78 | # === 0.1.1 == 79 | # - for bugfix releases, fixing typos in the docs, restructuring things, simply 80 | # anything that doesn't really change the behaviour of the software you 81 | # might use the third digit which is also sometimes called the build number. 82 | -------------------------------------------------------------------------------- /account_keeping/migrations/0007_auto_20190121_1334.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.17 on 2019-01-21 13:34 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('account_keeping', '0006_auto_20190102_1114'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='account', 17 | name='initial_amount', 18 | field=models.DecimalField(decimal_places=2, default=0, max_digits=18, verbose_name='Initial amount'), 19 | ), 20 | migrations.AlterField( 21 | model_name='account', 22 | name='total_amount', 23 | field=models.DecimalField(decimal_places=2, default=0, max_digits=18, verbose_name='Total amount'), 24 | ), 25 | migrations.AlterField( 26 | model_name='invoice', 27 | name='amount_gross', 28 | field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=18, verbose_name='Amount gross'), 29 | ), 30 | migrations.AlterField( 31 | model_name='invoice', 32 | name='amount_net', 33 | field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=18, verbose_name='Amount net'), 34 | ), 35 | migrations.AlterField( 36 | model_name='invoice', 37 | name='value_gross', 38 | field=models.DecimalField(decimal_places=2, default=0, max_digits=18, verbose_name='Value gross'), 39 | ), 40 | migrations.AlterField( 41 | model_name='invoice', 42 | name='value_net', 43 | field=models.DecimalField(decimal_places=2, default=0, max_digits=18, verbose_name='Value net'), 44 | ), 45 | migrations.AlterField( 46 | model_name='invoice', 47 | name='vat', 48 | field=models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='VAT'), 49 | ), 50 | migrations.AlterField( 51 | model_name='transaction', 52 | name='amount_gross', 53 | field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=18, verbose_name='Amount gross'), 54 | ), 55 | migrations.AlterField( 56 | model_name='transaction', 57 | name='amount_net', 58 | field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=18, verbose_name='Amount net'), 59 | ), 60 | migrations.AlterField( 61 | model_name='transaction', 62 | name='value_gross', 63 | field=models.DecimalField(decimal_places=2, default=0, max_digits=18, verbose_name='Value gross'), 64 | ), 65 | migrations.AlterField( 66 | model_name='transaction', 67 | name='value_net', 68 | field=models.DecimalField(decimal_places=2, default=0, max_digits=18, verbose_name='Value net'), 69 | ), 70 | migrations.AlterField( 71 | model_name='transaction', 72 | name='vat', 73 | field=models.DecimalField(decimal_places=2, default=0, max_digits=14, verbose_name='VAT'), 74 | ), 75 | ] 76 | -------------------------------------------------------------------------------- /account_keeping/forms.py: -------------------------------------------------------------------------------- 1 | """Forms of the account_keeping app.""" 2 | from django import forms 3 | from django.core.urlresolvers import reverse 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | from . import models 7 | 8 | 9 | class InvoiceForm(forms.ModelForm): 10 | class Meta: 11 | model = models.Invoice 12 | fields = '__all__' 13 | try: 14 | widgets = { 15 | 'invoice_date': forms.widgets.SelectDateWidget( 16 | attrs={'style': 'display: inline; width: auto;'}), 17 | 'payment_date': forms.widgets.SelectDateWidget( 18 | attrs={'style': 'display: inline; width: auto;'}), 19 | } 20 | except AttributeError: # pragma: nocover 21 | widgets = { 22 | 'invoice_date': forms.widgets.DateInput, 23 | 'payment_date': forms.widgets.DateInput, 24 | } 25 | 26 | def __init__(self, branch, *args, **kwargs): 27 | self.branch = branch 28 | super(InvoiceForm, self).__init__(*args, **kwargs) 29 | if branch or self.instance.pk: 30 | del self.fields['branch'] 31 | 32 | def save(self, *args, **kwargs): 33 | if not self.instance.pk and self.branch: 34 | self.instance.branch = self.branch 35 | return super(InvoiceForm, self).save(*args, **kwargs) 36 | 37 | 38 | class TransactionForm(forms.ModelForm): 39 | mark_invoice = forms.BooleanField( 40 | label=_('Mark invoice as paid?'), 41 | initial=True, 42 | required=False, 43 | widget=forms.widgets.CheckboxInput( 44 | attrs={'data-id': 'mark-invoice-field'}), 45 | ) 46 | 47 | class Meta: 48 | model = models.Transaction 49 | fields = '__all__' 50 | try: 51 | date_widget = forms.widgets.SelectDateWidget( 52 | attrs={'style': 'display: inline; width: auto;'}) 53 | except AttributeError: # pragma: nocover 54 | date_widget = forms.widgets.DateInput 55 | widgets = { 56 | 'transaction_date': date_widget, 57 | 'invoice': forms.widgets.NumberInput( 58 | attrs={'data-id': 'invoice-field'}), 59 | 'parent': forms.widgets.NumberInput(), 60 | } 61 | 62 | def __init__(self, branch, *args, **kwargs): 63 | super(TransactionForm, self).__init__(*args, **kwargs) 64 | self.fields['payee'].help_text = _( 65 | 'Add a payee').format( 66 | reverse('account_keeping_payee_create')) 67 | if branch: 68 | self.fields['account'].queryset = self.fields[ 69 | 'account'].queryset.filter(branch=branch) 70 | 71 | def save(self, *args, **kwargs): 72 | if self.instance.invoice and self.cleaned_data.get('mark_invoice'): 73 | # Set the payment date on related invoice 74 | self.instance.invoice.payment_date = self.instance.transaction_date 75 | self.instance.invoice.save() 76 | return super(TransactionForm, self).save(*args, **kwargs) 77 | 78 | 79 | class ExportForm(forms.Form): 80 | start = forms.DateField( 81 | label=_('Start'), 82 | ) 83 | 84 | end = forms.DateField( 85 | label=_('End'), 86 | ) 87 | -------------------------------------------------------------------------------- /account_keeping/migrations/0003_auto_20171223_0356.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.9 on 2017-12-23 03:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('account_keeping', '0002_auto_20161006_1403'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='account', 17 | name='initial_amount', 18 | field=models.DecimalField(decimal_places=10, default=0, max_digits=18, verbose_name='Initial amount'), 19 | ), 20 | migrations.AlterField( 21 | model_name='account', 22 | name='total_amount', 23 | field=models.DecimalField(decimal_places=10, default=0, max_digits=18, verbose_name='Total amount'), 24 | ), 25 | migrations.AlterField( 26 | model_name='invoice', 27 | name='amount_gross', 28 | field=models.DecimalField(blank=True, decimal_places=10, default=0, max_digits=18, verbose_name='Amount gross'), 29 | ), 30 | migrations.AlterField( 31 | model_name='invoice', 32 | name='amount_net', 33 | field=models.DecimalField(blank=True, decimal_places=10, default=0, max_digits=18, verbose_name='Amount net'), 34 | ), 35 | migrations.AlterField( 36 | model_name='invoice', 37 | name='value_gross', 38 | field=models.DecimalField(decimal_places=10, default=0, max_digits=18, verbose_name='Value gross'), 39 | ), 40 | migrations.AlterField( 41 | model_name='invoice', 42 | name='value_net', 43 | field=models.DecimalField(decimal_places=10, default=0, max_digits=18, verbose_name='Value net'), 44 | ), 45 | migrations.AlterField( 46 | model_name='invoice', 47 | name='vat', 48 | field=models.DecimalField(decimal_places=10, default=0, max_digits=14, verbose_name='VAT'), 49 | ), 50 | migrations.AlterField( 51 | model_name='transaction', 52 | name='amount_gross', 53 | field=models.DecimalField(blank=True, decimal_places=10, default=0, max_digits=18, verbose_name='Amount gross'), 54 | ), 55 | migrations.AlterField( 56 | model_name='transaction', 57 | name='amount_net', 58 | field=models.DecimalField(blank=True, decimal_places=10, default=0, max_digits=18, verbose_name='Amount net'), 59 | ), 60 | migrations.AlterField( 61 | model_name='transaction', 62 | name='value_gross', 63 | field=models.DecimalField(decimal_places=10, default=0, max_digits=18, verbose_name='Value gross'), 64 | ), 65 | migrations.AlterField( 66 | model_name='transaction', 67 | name='value_net', 68 | field=models.DecimalField(decimal_places=10, default=0, max_digits=18, verbose_name='Value net'), 69 | ), 70 | migrations.AlterField( 71 | model_name='transaction', 72 | name='vat', 73 | field=models.DecimalField(decimal_places=10, default=0, max_digits=14, verbose_name='VAT'), 74 | ), 75 | ] 76 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/partials/navigation.html: -------------------------------------------------------------------------------- 1 | {% load account_keeping_tags libs_tags i18n %} 2 |
3 | 66 |
67 | -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/year_view.html: -------------------------------------------------------------------------------- 1 | {% extends "account_keeping/base.html" %} 2 | {% load account_keeping_tags humanize i18n libs_tags tz %} 3 | 4 | {% block main %} 5 |
6 |

7 | {% trans "Year overview" %} ({{ year }}) 8 | {% if last_year %}{% endif %} 9 | {% if next_year %}{% endif %} 10 |

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for month in months %} 27 | 28 | {% localtime off %} 29 | 30 | {% endlocaltime %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% endfor %} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
{% trans "Month" %}{% trans "Income" %}{% trans "Expenses" %}{% trans "Profit" %}{% trans "New invoiced income" %}{% trans "Total outstanding profit" %}{% trans "Bank balance" %}{% trans "Total equity" %}
{{ month|date:"M" }}{% call income_total "get" month as income_total_value %}{{ income_total_value|default:0|currency:branch.currency }}{% call expenses_total "get" month as expenses_total_value %}{{ expenses_total_value|default:0|currency:branch.currency }}{% call profit_total "get" month as profit_total_value %}{{ profit_total_value|default:0|currency:branch.currency }}{% call new_total "get" month as new_total_value %}{{ new_total_value|default:0|currency:branch.currency }}{% call outstanding_total "get" month as outstanding_total_value %}{{ outstanding_total_value|default:0|currency:branch.currency }}{% call balance_total "get" month as balance_total_value %}{{ balance_total_value|default:0|currency:branch.currency }}{% call equity_total "get" month as equity_total_value %}{{ equity_total_value|default:0|currency:branch.currency }}
{% trans "Total" %}{{ income_total_total|currency:branch.currency }}{{ expenses_total_total|currency:branch.currency }}{{ profit_total_total|currency:branch.currency }}{{ new_total_total|currency:branch.currency }}
{% trans "Average" %}{{ income_average|currency:branch.currency }}{{ expenses_average|currency:branch.currency }}{{ profit_average|currency:branch.currency }}{{ new_average|currency:branch.currency }}{{ outstanding_average|currency:branch.currency }}{{ balance_average|currency}}{{ equity_average|currency:branch.currency }}
62 |
63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /account_keeping/management/commands/importer_mmex.py: -------------------------------------------------------------------------------- 1 | """ 2 | Importer that handles .csv files exported via Money Manager Ex. 3 | 4 | IMPORTANT: MMEX does not distinguish between incoming and outgoing "Transfer" 5 | transactions. After you export the .csv you must identify all incoming 6 | "Transfer" transactions and rename the type to "TransferDeposit". 7 | 8 | """ 9 | from decimal import Decimal 10 | import csv 11 | import datetime 12 | 13 | from django.core.management.base import BaseCommand, CommandError 14 | 15 | from currency_history.models import Currency 16 | 17 | from ... import models 18 | 19 | 20 | class Command(BaseCommand): 21 | help = 'Imports the specified .csv file from mmex' 22 | 23 | def add_arguments(self, parser): 24 | parser.add_argument( 25 | '-f', '--file', 26 | dest='filepath', 27 | help='Filepath to the .csv file that contains the data', 28 | ) 29 | parser.add_argument( 30 | '-a', '--account', 31 | dest='account', 32 | help='Account slug of the account that should hold the new data', 33 | ) 34 | parser.add_argument( 35 | '-c', '--currency', 36 | dest='currency', 37 | help='ISO-code of the currency for the specified data', 38 | ) 39 | parser.add_argument( 40 | '-t', '--vat', 41 | dest='vat', 42 | help='VAT that should be applied to all transactions (i.e. 19)', 43 | ) 44 | 45 | def handle(self, *args, **options): 46 | try: 47 | currency = Currency.objects.get(iso_code=options.get('currency')) 48 | except Currency.DoesNotExist: 49 | raise CommandError('The specified currency does not exist') 50 | 51 | account = models.Account.objects.get(slug=options.get('account')) 52 | vat = options.get('vat') 53 | if not vat: 54 | vat = 0 55 | vat = Decimal(vat) 56 | 57 | filepath = options.get('filepath') 58 | 59 | deposit = models.Transaction.TRANSACTION_TYPES['deposit'] 60 | withdrawal = models.Transaction.TRANSACTION_TYPES['withdrawal'] 61 | 62 | with open(filepath, 'rt') as csvfile: 63 | csvreader = csv.reader(csvfile) 64 | for row in csvreader: 65 | transaction_date = datetime.datetime.strptime( 66 | row[0], '%d/%m/%Y') 67 | payee, created = models.Payee.objects.get_or_create( 68 | name=row[1]) 69 | if row[2] in ['Withdrawal', 'Transfer']: 70 | transaction_type = withdrawal 71 | else: 72 | transaction_type = deposit 73 | amount = Decimal(row[3]) 74 | if row[4] and not row[5]: 75 | cat_name = row[4] 76 | else: 77 | cat_name = row[5] 78 | category, created = models.Category.objects.get_or_create( 79 | name=cat_name) 80 | description = row[7] 81 | 82 | invoice = models.Invoice.objects.create( 83 | invoice_type=transaction_type, 84 | invoice_date=datetime.date(1900, 1, 1), 85 | currency=currency, 86 | amount_gross=amount, 87 | vat=vat, 88 | payment_date=transaction_date, 89 | branch=models.Branch.objects.first(), 90 | ) 91 | 92 | models.Transaction.objects.create( 93 | account=account, 94 | transaction_type=transaction_type, 95 | transaction_date=transaction_date, 96 | description=description, 97 | invoice=invoice, 98 | payee=payee, 99 | category=category, 100 | currency=currency, 101 | amount_gross=amount, 102 | vat=vat, 103 | ) 104 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Account Keeping 2 | ====================== 3 | 4 | A reusable Django app for keeping track of transactions in your bank accounts. 5 | 6 | Installation 7 | ------------ 8 | 9 | To get the latest stable release from PyPi 10 | 11 | .. code-block:: bash 12 | 13 | pip install django-account-keeping 14 | 15 | To get the latest commit from GitHub 16 | 17 | .. code-block:: bash 18 | 19 | pip install -e git+git://github.com/bitmazk/django-account-keeping.git#egg=account_keeping 20 | 21 | 22 | Add all relevant apps to your ``INSTALLED_APPS`` 23 | 24 | .. code-block:: python 25 | 26 | INSTALLED_APPS = ( 27 | ..., 28 | 'account_keeping', 29 | 'currency_history', 30 | 'import_export', 31 | ) 32 | 33 | Add the ``account_keeping`` URLs to your ``urls.py`` 34 | 35 | .. code-block:: python 36 | 37 | urlpatterns = patterns('', 38 | ... 39 | url(r'^accounting/', include('account_keeping.urls')), 40 | ) 41 | 42 | Don't forget to migrate your database 43 | 44 | .. code-block:: bash 45 | 46 | ./manage.py migrate 47 | 48 | 49 | Usage 50 | ----- 51 | 52 | Configure currency history app 53 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 54 | 55 | Follow the instructions at: https://github.com/bitmazk/django-currency-history 56 | 57 | Make sure you add all needed currencies first. Second, define the wanted rates. 58 | Then make sure to get the latest rate history and add it by yourself. 59 | 60 | Add Account objects 61 | ^^^^^^^^^^^^^^^^^^^ 62 | 63 | Next you need to create your accounts. Note that the field `total_amount` is 64 | currently not used. It might eventually be used in the future for performance 65 | optimisations but at the moment it seems that computing the totals on the 66 | fly is fast enough. 67 | 68 | Import data from Money Manager Ex 69 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 70 | 71 | If you were using Money Manage Ex, you can export your data into a .csv file 72 | and then import it into this app:: 73 | 74 | ./manage.py importer_mmex -f filename.csv -c EUR -t 19 -a account-slug 75 | 76 | The parameter `-t` (VAT) is optional. If omitted, it is assumed that there is 77 | no VAT for the transactions in this account. 78 | 79 | IMPORTANT: Money Manager Ex has a transaction type `Transfer` but unfortunately 80 | in the `.csv` format the information of the source and destination accounts is 81 | lost. Here is a workaround: First you go through all your transactions in 82 | Money Manager Ex and those that have an incoming transfer (a deposit), you mark 83 | by adding some unique text to the description. Then you export the `.csv` and 84 | edit it in an editor. You search for your unique string and for those rows you 85 | change the transaction type from `Transfer` to `TransferDeposit`. 86 | 87 | Money Manager Ex does not have the notion of invoices, it only has 88 | transactions. When importing the data, this app will simply generate a dummy 89 | invoice for each transaction. Unfortunately, you have to go through all 90 | transactions manually and change the incoive date. 91 | 92 | Creating transactions with sub-transactions 93 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 94 | 95 | Sometimes a customer will pay several invoices within one transaction. For this 96 | case you should do the following: 97 | 98 | 1. Create the transaction that has appeared on your bank account as usual 99 | 2. For each invoice that has been paid, create a transaction that has the 100 | first transaction as a parent and of course create an invoice that is tied 101 | to it's transaction. 102 | 103 | Settings 104 | ^^^^^^^^ 105 | 106 | BASE_CURRENCY 107 | ************* 108 | 109 | Default: 'EUR' 110 | 111 | Define a default currency. All time statistics and summaries are displayed 112 | using this setting. 113 | 114 | Currently available views 115 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 116 | 117 | Alltime overview 118 | **************** 119 | 120 | URL: ../all/ 121 | 122 | Shows all transactions for all accounts, all time totals and outstanding 123 | invoices. 124 | 125 | Year overview 126 | ************* 127 | 128 | URL: ../YYYY/ 129 | 130 | Shows a table with total expenses, income, profit for each month of the year. 131 | Also shows how many new invoices have been sent to customers each month and 132 | how many invoices have been outstanding for each month. 133 | 134 | Shows the total bank balance for each month (at the end of each month) and 135 | total equity (bank balance + outstanding invoices). 136 | 137 | Month overview 138 | ************** 139 | 140 | URL: ../YYYY/MM/ 141 | 142 | Shows all transactions for all accounts for the given month. 143 | 144 | Contribute 145 | ---------- 146 | 147 | If you want to contribute to this project, please perform the following steps 148 | 149 | .. code-block:: bash 150 | 151 | # Fork this repository 152 | # Clone your fork 153 | mkvirtualenv -p python2.7 django-account-keeping 154 | make develop 155 | 156 | git co -b feature_branch master 157 | # Implement your feature and tests 158 | git add . && git commit 159 | git push -u origin feature_branch 160 | # Send us a pull request for your feature branch 161 | -------------------------------------------------------------------------------- /account_keeping/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.5 on 2016-05-30 08:01 3 | from __future__ import unicode_literals 4 | 5 | import account_keeping.models 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ('currency_history', '0001_initial'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Account', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('name', models.CharField(max_length=128)), 24 | ('slug', models.SlugField(max_length=128)), 25 | ('initial_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)), 26 | ('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)), 27 | ('currency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to='currency_history.Currency')), 28 | ], 29 | ), 30 | migrations.CreateModel( 31 | name='Category', 32 | fields=[ 33 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 34 | ('name', models.CharField(max_length=256)), 35 | ], 36 | options={ 37 | 'ordering': ['name'], 38 | }, 39 | ), 40 | migrations.CreateModel( 41 | name='Invoice', 42 | fields=[ 43 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 44 | ('invoice_type', models.CharField(choices=[(b'w', b'withdrawal'), (b'd', b'deposit')], max_length=1)), 45 | ('invoice_date', models.DateField()), 46 | ('invoice_number', models.CharField(blank=True, max_length=256)), 47 | ('description', models.TextField(blank=True)), 48 | ('amount_net', models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10)), 49 | ('vat', models.DecimalField(decimal_places=2, default=0, max_digits=4)), 50 | ('amount_gross', models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10)), 51 | ('value_net', models.DecimalField(decimal_places=2, default=0, max_digits=10)), 52 | ('value_gross', models.DecimalField(decimal_places=2, default=0, max_digits=10)), 53 | ('payment_date', models.DateField(blank=True, null=True)), 54 | ('pdf', models.FileField(blank=True, null=True, upload_to=b'invoice_files')), 55 | ('currency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='currency_history.Currency')), 56 | ], 57 | options={ 58 | 'ordering': ['-invoice_date'], 59 | }, 60 | bases=(account_keeping.models.AmountMixin, models.Model), 61 | ), 62 | migrations.CreateModel( 63 | name='Payee', 64 | fields=[ 65 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 66 | ('name', models.CharField(max_length=256)), 67 | ], 68 | options={ 69 | 'ordering': ['name'], 70 | }, 71 | ), 72 | migrations.CreateModel( 73 | name='Transaction', 74 | fields=[ 75 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 76 | ('transaction_type', models.CharField(choices=[(b'w', b'withdrawal'), (b'd', b'deposit')], max_length=1)), 77 | ('transaction_date', models.DateField()), 78 | ('description', models.TextField(blank=True)), 79 | ('invoice_number', models.CharField(blank=True, max_length=256)), 80 | ('amount_net', models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10)), 81 | ('vat', models.DecimalField(decimal_places=2, default=0, max_digits=4)), 82 | ('amount_gross', models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10)), 83 | ('value_net', models.DecimalField(decimal_places=2, default=0, max_digits=10)), 84 | ('value_gross', models.DecimalField(decimal_places=2, default=0, max_digits=10)), 85 | ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='account_keeping.Account')), 86 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='account_keeping.Category')), 87 | ('currency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='currency_history.Currency')), 88 | ('invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='account_keeping.Invoice')), 89 | ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='account_keeping.Transaction')), 90 | ('payee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='account_keeping.Payee')), 91 | ], 92 | options={ 93 | 'ordering': ['-transaction_date'], 94 | }, 95 | bases=(account_keeping.models.AmountMixin, models.Model), 96 | ), 97 | ] 98 | -------------------------------------------------------------------------------- /account_keeping/south_migrations/0002_auto__add_field_invoice_description.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from south.utils import datetime_utils as datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding field 'Invoice.description' 12 | db.add_column(u'account_keeping_invoice', 'description', 13 | self.gf('django.db.models.fields.TextField')(default='', blank=True), 14 | keep_default=False) 15 | 16 | 17 | def backwards(self, orm): 18 | # Deleting field 'Invoice.description' 19 | db.delete_column(u'account_keeping_invoice', 'description') 20 | 21 | 22 | models = { 23 | u'account_keeping.account': { 24 | 'Meta': {'object_name': 'Account'}, 25 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'accounts'", 'to': u"orm['account_keeping.Currency']"}), 26 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 27 | 'initial_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 28 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 29 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '128'}), 30 | 'total_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}) 31 | }, 32 | u'account_keeping.category': { 33 | 'Meta': {'ordering': "['name']", 'object_name': 'Category'}, 34 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 35 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}) 36 | }, 37 | u'account_keeping.currency': { 38 | 'Meta': {'object_name': 'Currency'}, 39 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 40 | 'is_base_currency': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 41 | 'iso_code': ('django.db.models.fields.CharField', [], {'max_length': '3'}), 42 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}) 43 | }, 44 | u'account_keeping.currencyrate': { 45 | 'Meta': {'object_name': 'CurrencyRate'}, 46 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['account_keeping.Currency']"}), 47 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 48 | 'month': ('django.db.models.fields.PositiveIntegerField', [], {}), 49 | 'rate': ('django.db.models.fields.DecimalField', [], {'max_digits': '18', 'decimal_places': '8'}), 50 | 'year': ('django.db.models.fields.PositiveIntegerField', [], {}) 51 | }, 52 | u'account_keeping.invoice': { 53 | 'Meta': {'ordering': "['invoice_date']", 'object_name': 'Invoice'}, 54 | 'amount_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 55 | 'amount_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 56 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invoices'", 'to': u"orm['account_keeping.Currency']"}), 57 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 58 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 59 | 'invoice_date': ('django.db.models.fields.DateField', [], {}), 60 | 'invoice_number': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), 61 | 'invoice_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 62 | 'payment_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), 63 | 'pdf': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 64 | 'vat': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '4', 'decimal_places': '2'}) 65 | }, 66 | u'account_keeping.payee': { 67 | 'Meta': {'ordering': "['name']", 'object_name': 'Payee'}, 68 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 69 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}) 70 | }, 71 | u'account_keeping.transaction': { 72 | 'Meta': {'ordering': "['transaction_date']", 'object_name': 'Transaction'}, 73 | 'account': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Account']"}), 74 | 'amount_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 75 | 'amount_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 76 | 'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Category']"}), 77 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Currency']"}), 78 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 79 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 80 | 'invoice': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'transactions'", 'null': 'True', 'to': u"orm['account_keeping.Invoice']"}), 81 | 'invoice_number': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), 82 | 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['account_keeping.Transaction']"}), 83 | 'payee': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Payee']"}), 84 | 'transaction_date': ('django.db.models.fields.DateField', [], {}), 85 | 'transaction_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 86 | 'value_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 87 | 'value_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 88 | 'vat': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '4', 'decimal_places': '2'}) 89 | } 90 | } 91 | 92 | complete_apps = ['account_keeping'] -------------------------------------------------------------------------------- /account_keeping/south_migrations/0004_init_invoice_value_fields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from south.utils import datetime_utils as datetime 3 | from south.db import db 4 | from south.v2 import DataMigration 5 | from django.db import models 6 | 7 | class Migration(DataMigration): 8 | 9 | def forwards(self, orm): 10 | "Write your forwards methods here." 11 | # Note: Don't use "from appname.models import ModelName". 12 | # Use orm.ModelName to refer to models in this application, 13 | # and orm['appname.ModelName'] for models in other applications. 14 | from account_keeping.models import Invoice 15 | for invoice in Invoice.objects.all(): 16 | invoice.save() 17 | 18 | def backwards(self, orm): 19 | "Write your backwards methods here." 20 | 21 | models = { 22 | u'account_keeping.account': { 23 | 'Meta': {'object_name': 'Account'}, 24 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'accounts'", 'to': u"orm['account_keeping.Currency']"}), 25 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 26 | 'initial_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 27 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 28 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '128'}), 29 | 'total_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}) 30 | }, 31 | u'account_keeping.category': { 32 | 'Meta': {'ordering': "['name']", 'object_name': 'Category'}, 33 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 34 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}) 35 | }, 36 | u'account_keeping.currency': { 37 | 'Meta': {'object_name': 'Currency'}, 38 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 39 | 'is_base_currency': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 40 | 'iso_code': ('django.db.models.fields.CharField', [], {'max_length': '3'}), 41 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}) 42 | }, 43 | u'account_keeping.currencyrate': { 44 | 'Meta': {'object_name': 'CurrencyRate'}, 45 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['account_keeping.Currency']"}), 46 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 47 | 'month': ('django.db.models.fields.PositiveIntegerField', [], {}), 48 | 'rate': ('django.db.models.fields.DecimalField', [], {'max_digits': '18', 'decimal_places': '8'}), 49 | 'year': ('django.db.models.fields.PositiveIntegerField', [], {}) 50 | }, 51 | u'account_keeping.invoice': { 52 | 'Meta': {'ordering': "['invoice_date']", 'object_name': 'Invoice'}, 53 | 'amount_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 54 | 'amount_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 55 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invoices'", 'to': u"orm['account_keeping.Currency']"}), 56 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 57 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 58 | 'invoice_date': ('django.db.models.fields.DateField', [], {}), 59 | 'invoice_number': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), 60 | 'invoice_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 61 | 'payment_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), 62 | 'pdf': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 63 | 'value_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 64 | 'value_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 65 | 'vat': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '4', 'decimal_places': '2'}) 66 | }, 67 | u'account_keeping.payee': { 68 | 'Meta': {'ordering': "['name']", 'object_name': 'Payee'}, 69 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 70 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}) 71 | }, 72 | u'account_keeping.transaction': { 73 | 'Meta': {'ordering': "['transaction_date']", 'object_name': 'Transaction'}, 74 | 'account': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Account']"}), 75 | 'amount_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 76 | 'amount_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 77 | 'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Category']"}), 78 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Currency']"}), 79 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 80 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 81 | 'invoice': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'transactions'", 'null': 'True', 'to': u"orm['account_keeping.Invoice']"}), 82 | 'invoice_number': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), 83 | 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['account_keeping.Transaction']"}), 84 | 'payee': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Payee']"}), 85 | 'transaction_date': ('django.db.models.fields.DateField', [], {}), 86 | 'transaction_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 87 | 'value_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 88 | 'value_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 89 | 'vat': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '4', 'decimal_places': '2'}) 90 | } 91 | } 92 | 93 | complete_apps = ['account_keeping'] 94 | symmetrical = True 95 | -------------------------------------------------------------------------------- /account_keeping/south_migrations/0003_auto__add_field_invoice_value_net__add_field_invoice_value_gross.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from south.utils import datetime_utils as datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding field 'Invoice.value_net' 12 | db.add_column(u'account_keeping_invoice', 'value_net', 13 | self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=10, decimal_places=2), 14 | keep_default=False) 15 | 16 | # Adding field 'Invoice.value_gross' 17 | db.add_column(u'account_keeping_invoice', 'value_gross', 18 | self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=10, decimal_places=2), 19 | keep_default=False) 20 | 21 | 22 | def backwards(self, orm): 23 | # Deleting field 'Invoice.value_net' 24 | db.delete_column(u'account_keeping_invoice', 'value_net') 25 | 26 | # Deleting field 'Invoice.value_gross' 27 | db.delete_column(u'account_keeping_invoice', 'value_gross') 28 | 29 | 30 | models = { 31 | u'account_keeping.account': { 32 | 'Meta': {'object_name': 'Account'}, 33 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'accounts'", 'to': u"orm['account_keeping.Currency']"}), 34 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 35 | 'initial_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 36 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 37 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '128'}), 38 | 'total_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}) 39 | }, 40 | u'account_keeping.category': { 41 | 'Meta': {'ordering': "['name']", 'object_name': 'Category'}, 42 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 43 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}) 44 | }, 45 | u'account_keeping.currency': { 46 | 'Meta': {'object_name': 'Currency'}, 47 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 48 | 'is_base_currency': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 49 | 'iso_code': ('django.db.models.fields.CharField', [], {'max_length': '3'}), 50 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}) 51 | }, 52 | u'account_keeping.currencyrate': { 53 | 'Meta': {'object_name': 'CurrencyRate'}, 54 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['account_keeping.Currency']"}), 55 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 56 | 'month': ('django.db.models.fields.PositiveIntegerField', [], {}), 57 | 'rate': ('django.db.models.fields.DecimalField', [], {'max_digits': '18', 'decimal_places': '8'}), 58 | 'year': ('django.db.models.fields.PositiveIntegerField', [], {}) 59 | }, 60 | u'account_keeping.invoice': { 61 | 'Meta': {'ordering': "['invoice_date']", 'object_name': 'Invoice'}, 62 | 'amount_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 63 | 'amount_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 64 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invoices'", 'to': u"orm['account_keeping.Currency']"}), 65 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 66 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 67 | 'invoice_date': ('django.db.models.fields.DateField', [], {}), 68 | 'invoice_number': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), 69 | 'invoice_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 70 | 'payment_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), 71 | 'pdf': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 72 | 'value_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 73 | 'value_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 74 | 'vat': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '4', 'decimal_places': '2'}) 75 | }, 76 | u'account_keeping.payee': { 77 | 'Meta': {'ordering': "['name']", 'object_name': 'Payee'}, 78 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 79 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}) 80 | }, 81 | u'account_keeping.transaction': { 82 | 'Meta': {'ordering': "['transaction_date']", 'object_name': 'Transaction'}, 83 | 'account': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Account']"}), 84 | 'amount_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 85 | 'amount_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 86 | 'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Category']"}), 87 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Currency']"}), 88 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 89 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 90 | 'invoice': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'transactions'", 'null': 'True', 'to': u"orm['account_keeping.Invoice']"}), 91 | 'invoice_number': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), 92 | 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['account_keeping.Transaction']"}), 93 | 'payee': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Payee']"}), 94 | 'transaction_date': ('django.db.models.fields.DateField', [], {}), 95 | 'transaction_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 96 | 'value_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 97 | 'value_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 98 | 'vat': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '4', 'decimal_places': '2'}) 99 | } 100 | } 101 | 102 | complete_apps = ['account_keeping'] -------------------------------------------------------------------------------- /account_keeping/south_migrations/0006_auto__del_currencyrate__del_currency.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from south.utils import datetime_utils as datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Deleting model 'CurrencyRate' 12 | db.delete_table(u'account_keeping_currencyrate') 13 | 14 | # Deleting model 'Currency' 15 | db.delete_table(u'account_keeping_currency') 16 | 17 | 18 | def backwards(self, orm): 19 | # Adding model 'CurrencyRate' 20 | db.create_table(u'account_keeping_currencyrate', ( 21 | ('month', self.gf('django.db.models.fields.PositiveIntegerField')()), 22 | ('currency', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['account_keeping.Currency'])), 23 | ('rate', self.gf('django.db.models.fields.DecimalField')(max_digits=18, decimal_places=8)), 24 | ('year', self.gf('django.db.models.fields.PositiveIntegerField')()), 25 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 26 | )) 27 | db.send_create_signal(u'account_keeping', ['CurrencyRate']) 28 | 29 | # Adding model 'Currency' 30 | db.create_table(u'account_keeping_currency', ( 31 | ('iso_code', self.gf('django.db.models.fields.CharField')(max_length=3)), 32 | ('is_base_currency', self.gf('django.db.models.fields.BooleanField')(default=False)), 33 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 34 | ('name', self.gf('django.db.models.fields.CharField')(max_length=64)), 35 | )) 36 | db.send_create_signal(u'account_keeping', ['Currency']) 37 | 38 | 39 | models = { 40 | u'account_keeping.account': { 41 | 'Meta': {'object_name': 'Account'}, 42 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'accounts'", 'to': u"orm['currency_history.Currency']"}), 43 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 44 | 'initial_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 45 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 46 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '128'}), 47 | 'total_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}) 48 | }, 49 | u'account_keeping.category': { 50 | 'Meta': {'ordering': "['name']", 'object_name': 'Category'}, 51 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 52 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}) 53 | }, 54 | u'account_keeping.invoice': { 55 | 'Meta': {'ordering': "['-invoice_date']", 'object_name': 'Invoice'}, 56 | 'amount_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 57 | 'amount_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 58 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invoices'", 'to': u"orm['currency_history.Currency']"}), 59 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 60 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 61 | 'invoice_date': ('django.db.models.fields.DateField', [], {}), 62 | 'invoice_number': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), 63 | 'invoice_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 64 | 'payment_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), 65 | 'pdf': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 66 | 'value_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 67 | 'value_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 68 | 'vat': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '4', 'decimal_places': '2'}) 69 | }, 70 | u'account_keeping.payee': { 71 | 'Meta': {'ordering': "['name']", 'object_name': 'Payee'}, 72 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 73 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}) 74 | }, 75 | u'account_keeping.transaction': { 76 | 'Meta': {'ordering': "['-transaction_date']", 'object_name': 'Transaction'}, 77 | 'account': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Account']"}), 78 | 'amount_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 79 | 'amount_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 80 | 'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Category']"}), 81 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['currency_history.Currency']"}), 82 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 83 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 84 | 'invoice': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'transactions'", 'null': 'True', 'to': u"orm['account_keeping.Invoice']"}), 85 | 'invoice_number': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), 86 | 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['account_keeping.Transaction']"}), 87 | 'payee': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Payee']"}), 88 | 'transaction_date': ('django.db.models.fields.DateField', [], {}), 89 | 'transaction_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 90 | 'value_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 91 | 'value_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 92 | 'vat': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '4', 'decimal_places': '2'}) 93 | }, 94 | u'currency_history.currency': { 95 | 'Meta': {'ordering': "['iso_code']", 'object_name': 'Currency'}, 96 | 'abbreviation': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'}), 97 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 98 | 'iso_code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '3'}), 99 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 100 | } 101 | } 102 | 103 | complete_apps = ['account_keeping'] -------------------------------------------------------------------------------- /account_keeping/templates/account_keeping/accounts_view.html: -------------------------------------------------------------------------------- 1 | {% extends "account_keeping/base.html" %} 2 | {% load account_keeping_tags humanize i18n libs_tags %} 3 | 4 | {% block main %} 5 |
6 |

7 | {{ view_name }} 8 | {% if last_month %}{% endif %} 9 | {% if next_month %}{% endif %} 10 |

11 | 12 | 19 | 20 |
21 | {% for value_dict in account_transactions %} 22 |
23 |

{{ value_dict.account.name }} ({{ value_dict.account.currency.iso_code }})

24 | {% trans "Add new transaction" %} 25 | {% save "BALANCE" value_dict.account_balance %} 26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
{% trans "net" %}{% trans "gross" %}
{% trans "Income total" %}{{ value_dict.income_net_total|currency:value_dict.account.currency }}{{ value_dict.income_gross_total|currency:value_dict.account.currency }}
{% trans "Expenses total" %}{{ value_dict.expenses_net_total|currency:value_dict.account.currency }}{{ value_dict.expenses_gross_total|currency:value_dict.account.currency }}
{% trans "Profit total" %}{{ value_dict.amount_net_total|currency:value_dict.account.currency }}{{ value_dict.amount_gross_total|currency:value_dict.account.currency }}
55 |
56 |
57 |

{% trans "Current balance" %}

58 |

{{ BALANCE|currency:value_dict.account.currency }}

59 |
60 |
61 | 62 | 63 | {% include "account_keeping/partials/transactions_table_head.html" with show_balance=1 %} 64 | 65 | {% include "account_keeping/partials/transactions_table_body.html" with show_balance=1 transactions=value_dict.transactions %} 66 | 67 |
68 | {% trans "Add new transaction" %} 69 |
70 | {% endfor %} 71 | 72 |
73 |

{% trans "Total" %} ({{ base_currency }})

74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | {% for value_dict in account_transactions %} 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | {% endfor %} 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
{% trans "Account" %}{% trans "Expenses (net)" %}{% trans "Expenses (gross)" %}{% trans "Income (net)" %}{% trans "Income (gross)" %}{% trans "Profit (net)" %}{% trans "Profit (gross)" %}
{{ value_dict.account.name }}{{ value_dict.expenses_net_sum_base|currency:value_dict.account.currency }}{{ value_dict.expenses_gross_sum_base|currency:value_dict.account.currency }}{{ value_dict.income_net_sum_base|currency:value_dict.account.currency }}{{ value_dict.income_gross_sum_base|currency:value_dict.account.currency }}{{ value_dict.amount_net_sum_base|currency:value_dict.account.currency }}{{ value_dict.amount_gross_sum_base|currency:value_dict.account.currency }}
{% trans "Total" %}{{ totals.expenses_net|currency:branch.currency}}{{ totals.expenses_gross|currency:branch.currency}}{{ totals.income_net|currency:branch.currency}}{{ totals.income_gross|currency:branch.currency}}{{ totals.amount_net|currency:branch.currency}}{{ totals.amount_gross|currency:branch.currency}}
110 | 111 |

{% trans "Outstanding total" %} (CCY / {{ base_currency }})

112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | {% for currency, ccy_totals in outstanding_ccy_totals.items %} 124 | 125 | 126 | 129 | 132 | 135 | 136 | {% endfor %} 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 |
{% trans "Currency" %}{% trans "Expenses (gross)" %}{% trans "Income (gross)" %}{% trans "Profit (gross)" %}
{{ currency.iso_code }} 127 | {% include "account_keeping/partials/currency_base_pair.html" with amount=ccy_totals.expenses_gross base_amount=ccy_totals.expenses_gross_base %} 128 | 130 | {% include "account_keeping/partials/currency_base_pair.html" with amount=ccy_totals.income_gross base_amount=ccy_totals.income_gross_base %} 131 | 133 | {% include "account_keeping/partials/currency_base_pair.html" with amount=ccy_totals.profit_gross base_amount=ccy_totals.profit_gross_base %} 134 |
{% trans "Total" %} ({{ base_currency.iso_code }}){{ totals.outstanding_expenses_gross|currency:branch.currency}}{{ totals.outstanding_income_gross|currency:branch.currency}}{{ totals.outstanding_profit_gross|currency:branch.currency}}
145 |
146 | 147 |
148 |

{% trans "Outstanding invoices" %} ({{ outstanding_invoices.count }})

149 | {% trans "Add new invoice" %} 150 | {% include "account_keeping/partials/invoices_table.html" with invoices=outstanding_invoices %} 151 | {% trans "Add new invoice" %} 152 |
153 |
154 |
155 | {% endblock %} 156 | -------------------------------------------------------------------------------- /account_keeping/tests/models_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the models of the account_keeping app.""" 2 | from django.test import TestCase 3 | from django.utils.timezone import now, timedelta 4 | 5 | from mixer.backend.django import mixer 6 | 7 | from .. import models 8 | 9 | 10 | WITHDRAWAL = models.Transaction.TRANSACTION_TYPES['withdrawal'] 11 | DEPOSIT = models.Transaction.TRANSACTION_TYPES['deposit'] 12 | 13 | 14 | class BranchTestCase(TestCase): 15 | """Tests for the ``Account`` model.""" 16 | def setUp(self): 17 | self.branch = mixer.blend('account_keeping.Branch') 18 | 19 | def test_model(self): 20 | self.assertTrue(str(self.branch)) 21 | 22 | 23 | class AccountTestCase(TestCase): 24 | """Tests for the ``Account`` model.""" 25 | def setUp(self): 26 | self.account = mixer.blend('account_keeping.Account') 27 | 28 | def test_model(self): 29 | self.assertTrue(str(self.account)) 30 | 31 | def test_get_balance(self): 32 | self.assertEqual(self.account.get_balance(), 0) 33 | 34 | 35 | class InvoiceTestCase(TestCase): 36 | """Tests for the ``Invoice`` model.""" 37 | def test_model(self): 38 | obj = mixer.blend('account_keeping.Invoice', invoice_number='') 39 | self.assertTrue(str(obj)) 40 | obj.invoice_number = 'Foo123' 41 | obj.save() 42 | self.assertEqual(str(obj), 'Foo123') 43 | 44 | def test_manager(self): 45 | mixer.blend('account_keeping.Invoice') 46 | mixer.blend('account_keeping.Invoice') 47 | self.assertEqual(models.Invoice.objects.get_without_pdf().count(), 2) 48 | 49 | def test_balance(self): 50 | invoice = mixer.blend('account_keeping.Invoice', amount_net=100) 51 | self.assertEqual(int(invoice.balance), -100) 52 | mixer.blend('account_keeping.Transaction', amount_net=10, 53 | invoice=invoice, currency=invoice.currency) 54 | transaction = mixer.blend('account_keeping.Transaction', amount_net=50, 55 | invoice=invoice) 56 | mixer.blend('currency_history.CurrencyRateHistory', value=0.5, 57 | rate__from_currency=transaction.currency, 58 | rate__to_currency=invoice.currency) 59 | self.assertEqual(int(invoice.balance), -65) 60 | 61 | 62 | class PayeeTestCase(TestCase): 63 | """Tests for the ``Payee`` model.""" 64 | def setUp(self): 65 | self.payee = mixer.blend('account_keeping.Payee') 66 | 67 | def test_model(self): 68 | self.assertTrue(str(self.payee)) 69 | 70 | def test_invoices(self): 71 | self.assertEqual(self.payee.invoices().count(), 0) 72 | 73 | 74 | class CategoryTestCase(TestCase): 75 | """Tests for the ``Category`` model.""" 76 | def test_model(self): 77 | obj = mixer.blend('account_keeping.Category') 78 | self.assertTrue(str(obj)) 79 | 80 | 81 | class TransactionTestCase(TestCase): 82 | """Tests for the ``Transaction`` model.""" 83 | longMessage = True 84 | 85 | def setUp(self): 86 | self.transaction = mixer.blend('account_keeping.Transaction', 87 | invoice_number='') 88 | 89 | def test_model(self): 90 | self.assertTrue(str(self.transaction)) 91 | self.transaction.invoice = mixer.blend('account_keeping.Invoice') 92 | self.assertEqual(str(self.transaction), 93 | self.transaction.invoice.invoice_number) 94 | self.transaction.invoice_number = 'Foo123' 95 | self.assertEqual(str(self.transaction), 'Foo123') 96 | 97 | def test_save(self): 98 | obj = mixer.blend( 99 | 'account_keeping.Transaction', 100 | amount_net=100, vat=19, amount_gross=None) 101 | self.assertEqual(obj.amount_gross, 119, msg=( 102 | 'If only amount_net is given, amount_gross should be calculated')) 103 | 104 | obj = mixer.blend( 105 | 'account_keeping.Transaction', 106 | amount_net=100, vat=0, amount_gross=None) 107 | self.assertEqual(obj.amount_gross, 100, msg=( 108 | 'If only amount_net is given and VAT is 0, amount_gross should be' 109 | ' identical to amount_net')) 110 | 111 | obj = mixer.blend( 112 | 'account_keeping.Transaction', 113 | amount_net=None, vat=19, amount_gross=119) 114 | self.assertEqual(obj.amount_net, 100, msg=( 115 | 'If only amount_gross is given, amount_net should be calculated')) 116 | 117 | obj = mixer.blend( 118 | 'account_keeping.Transaction', 119 | amount_net=None, vat=0, amount_gross=119) 120 | self.assertEqual(obj.amount_gross, 119, msg=( 121 | 'If only amount_gross is given and VAT is 0, amount_net should be' 122 | ' identical to amount_gross')) 123 | 124 | obj = mixer.blend('account_keeping.Transaction', 125 | transaction_type=DEPOSIT) 126 | self.assertEqual(obj.value_net, obj.amount_net, msg=( 127 | 'When type is deposit, the value should be positive')) 128 | 129 | obj = mixer.blend('account_keeping.Transaction', 130 | transaction_type=WITHDRAWAL) 131 | self.assertEqual(obj.value_net, obj.amount_net * -1, msg=( 132 | 'When type is withdrawal, the value should be negative')) 133 | 134 | def test_get_description(self): 135 | """Tests for the ``get_description`` method.""" 136 | trans = mixer.blend('account_keeping.Transaction', description='') 137 | result = trans.get_description() 138 | self.assertEqual(result, 'n/a', msg=( 139 | 'If no description is set, it should return `n/a`')) 140 | 141 | trans.description = 'foo' 142 | trans.save() 143 | result = trans.get_description() 144 | self.assertEqual(result, 'foo', msg=( 145 | 'If description is set, it should return the description')) 146 | 147 | invoice = mixer.blend('account_keeping.Invoice', description='barfoo') 148 | trans = mixer.blend('account_keeping.Transaction', invoice=invoice, 149 | description='') 150 | result = trans.get_description() 151 | self.assertEqual(result, 'barfoo', msg=( 152 | 'If no description is set but the invoice has one, it should' 153 | ' return that one')) 154 | 155 | trans = mixer.blend('account_keeping.Transaction', description='') 156 | mixer.blend('account_keeping.Transaction', parent=trans, 157 | description='foofoo', 158 | transaction_date=now() - timedelta(days=2)) 159 | mixer.blend('account_keeping.Transaction', parent=trans, 160 | description='barbar', 161 | transaction_date=now() - timedelta(days=1)) 162 | result = trans.get_description() 163 | self.assertEqual(result, 'barbar,\nfoofoo,\n', msg=( 164 | 'If no description is set but children have one, it should' 165 | ' return the descriptions of the children')) 166 | 167 | trans = mixer.blend('account_keeping.Transaction', description='') 168 | mixer.blend('account_keeping.Transaction', 169 | parent=trans, invoice=invoice, description='') 170 | mixer.blend('account_keeping.Transaction', parent=trans, 171 | description='barbar', 172 | transaction_date=now() - timedelta(days=1)) 173 | result = trans.get_description() 174 | self.assertEqual(result, 'barbar,\nbarfoo,\n', msg=( 175 | 'If no description is set and children are present, but a child' 176 | ' doesn\'t have a description, it should return the description of' 177 | ' the child\'s invoice')) 178 | 179 | def test_get_invoices(self): 180 | """Tests for the ``get_invoices`` method.""" 181 | trans = mixer.blend('account_keeping.Transaction') 182 | result = trans.get_invoices() 183 | self.assertEqual(result, [None, ], msg=( 184 | 'If it doesn`t have an invoice, it should return a list with' 185 | ' None')) 186 | 187 | invoice = mixer.blend('account_keeping.Invoice') 188 | trans.invoice = invoice 189 | trans.save 190 | result = trans.get_invoices() 191 | self.assertEqual(result, [invoice, ], msg=( 192 | 'If it has an invoice, it should return a list with the invoice')) 193 | 194 | trans = mixer.blend('account_keeping.Transaction') 195 | mixer.blend('account_keeping.Transaction', 196 | parent=trans, invoice=invoice) 197 | result = trans.get_invoices() 198 | self.assertEqual(result, [invoice, ], msg=( 199 | 'If it does not have an invoice, it should return the invoices of' 200 | ' it`s children')) 201 | 202 | def test_get_totals_by_payee(self): 203 | """Tests for the ``get_totals_by_payee`` method.""" 204 | trans1a = mixer.blend('account_keeping.Transaction', value_gross=11.9) 205 | mixer.blend('account_keeping.Transaction', value_gross=11.9, 206 | account=trans1a.account, payee=trans1a.payee) 207 | mixer.blend('account_keeping.Transaction', account=trans1a.account, 208 | value_gross=11.9) 209 | result = models.Transaction.objects.get_totals_by_payee( 210 | trans1a.account) 211 | self.assertEqual(result.count(), 2, msg=( 212 | 'Should return all transactions grouped by payee')) 213 | 214 | def test_manager(self): 215 | self.assertEqual( 216 | models.Transaction.objects.get_without_invoice().count(), 1) 217 | -------------------------------------------------------------------------------- /account_keeping/migrations/0002_auto_20161006_1403.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.9 on 2016-10-06 14:03 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('account_keeping', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterModelOptions( 17 | name='account', 18 | options={'ordering': ['name'], 'verbose_name': 'Account', 'verbose_name_plural': 'Accounts'}, 19 | ), 20 | migrations.AlterModelOptions( 21 | name='category', 22 | options={'ordering': ['name'], 'verbose_name': 'Category', 'verbose_name_plural': 'Categories'}, 23 | ), 24 | migrations.AlterModelOptions( 25 | name='invoice', 26 | options={'ordering': ['-invoice_date', '-pk'], 'verbose_name': 'Invoice', 'verbose_name_plural': 'Invoices'}, 27 | ), 28 | migrations.AlterModelOptions( 29 | name='payee', 30 | options={'ordering': ['name'], 'verbose_name': 'Payee', 'verbose_name_plural': 'Payees'}, 31 | ), 32 | migrations.AlterModelOptions( 33 | name='transaction', 34 | options={'ordering': ['-transaction_date', '-pk'], 'verbose_name': 'Transaction', 'verbose_name_plural': 'Transactions'}, 35 | ), 36 | migrations.AddField( 37 | model_name='account', 38 | name='active', 39 | field=models.BooleanField(default=True, verbose_name='Active?'), 40 | ), 41 | migrations.AlterField( 42 | model_name='account', 43 | name='currency', 44 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to='currency_history.Currency', verbose_name='Currency'), 45 | ), 46 | migrations.AlterField( 47 | model_name='account', 48 | name='initial_amount', 49 | field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Initial amount'), 50 | ), 51 | migrations.AlterField( 52 | model_name='account', 53 | name='name', 54 | field=models.CharField(max_length=128, verbose_name='Name'), 55 | ), 56 | migrations.AlterField( 57 | model_name='account', 58 | name='slug', 59 | field=models.SlugField(max_length=128, verbose_name='Slug'), 60 | ), 61 | migrations.AlterField( 62 | model_name='account', 63 | name='total_amount', 64 | field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Total amount'), 65 | ), 66 | migrations.AlterField( 67 | model_name='category', 68 | name='name', 69 | field=models.CharField(max_length=256, verbose_name='Name'), 70 | ), 71 | migrations.AlterField( 72 | model_name='invoice', 73 | name='amount_gross', 74 | field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10, verbose_name='Amount gross'), 75 | ), 76 | migrations.AlterField( 77 | model_name='invoice', 78 | name='amount_net', 79 | field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10, verbose_name='Amount net'), 80 | ), 81 | migrations.AlterField( 82 | model_name='invoice', 83 | name='currency', 84 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='currency_history.Currency', verbose_name='Currency'), 85 | ), 86 | migrations.AlterField( 87 | model_name='invoice', 88 | name='description', 89 | field=models.TextField(blank=True, verbose_name='Description'), 90 | ), 91 | migrations.AlterField( 92 | model_name='invoice', 93 | name='invoice_date', 94 | field=models.DateField(verbose_name='Invoice date'), 95 | ), 96 | migrations.AlterField( 97 | model_name='invoice', 98 | name='invoice_number', 99 | field=models.CharField(blank=True, max_length=256, verbose_name='Invoice No.'), 100 | ), 101 | migrations.AlterField( 102 | model_name='invoice', 103 | name='invoice_type', 104 | field=models.CharField(choices=[('w', 'withdrawal'), ('d', 'deposit')], max_length=1, verbose_name='Invoice type'), 105 | ), 106 | migrations.AlterField( 107 | model_name='invoice', 108 | name='payment_date', 109 | field=models.DateField(blank=True, null=True, verbose_name='Payment date'), 110 | ), 111 | migrations.AlterField( 112 | model_name='invoice', 113 | name='pdf', 114 | field=models.FileField(blank=True, null=True, upload_to='invoice_files', verbose_name='PDF'), 115 | ), 116 | migrations.AlterField( 117 | model_name='invoice', 118 | name='value_gross', 119 | field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Value gross'), 120 | ), 121 | migrations.AlterField( 122 | model_name='invoice', 123 | name='value_net', 124 | field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Value net'), 125 | ), 126 | migrations.AlterField( 127 | model_name='invoice', 128 | name='vat', 129 | field=models.DecimalField(decimal_places=2, default=0, max_digits=4, verbose_name='VAT'), 130 | ), 131 | migrations.AlterField( 132 | model_name='payee', 133 | name='name', 134 | field=models.CharField(max_length=256, verbose_name='Payee'), 135 | ), 136 | migrations.AlterField( 137 | model_name='transaction', 138 | name='account', 139 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='account_keeping.Account', verbose_name='Account'), 140 | ), 141 | migrations.AlterField( 142 | model_name='transaction', 143 | name='amount_gross', 144 | field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10, verbose_name='Amount gross'), 145 | ), 146 | migrations.AlterField( 147 | model_name='transaction', 148 | name='amount_net', 149 | field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10, verbose_name='Amount net'), 150 | ), 151 | migrations.AlterField( 152 | model_name='transaction', 153 | name='category', 154 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='account_keeping.Category', verbose_name='Category'), 155 | ), 156 | migrations.AlterField( 157 | model_name='transaction', 158 | name='currency', 159 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='currency_history.Currency', verbose_name='Currency'), 160 | ), 161 | migrations.AlterField( 162 | model_name='transaction', 163 | name='description', 164 | field=models.TextField(blank=True, verbose_name='Description'), 165 | ), 166 | migrations.AlterField( 167 | model_name='transaction', 168 | name='invoice', 169 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='account_keeping.Invoice', verbose_name='Invoice'), 170 | ), 171 | migrations.AlterField( 172 | model_name='transaction', 173 | name='invoice_number', 174 | field=models.CharField(blank=True, max_length=256, verbose_name='Invoice No.'), 175 | ), 176 | migrations.AlterField( 177 | model_name='transaction', 178 | name='parent', 179 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='account_keeping.Transaction', verbose_name='Parent'), 180 | ), 181 | migrations.AlterField( 182 | model_name='transaction', 183 | name='payee', 184 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='account_keeping.Payee', verbose_name='Payee'), 185 | ), 186 | migrations.AlterField( 187 | model_name='transaction', 188 | name='transaction_date', 189 | field=models.DateField(verbose_name='Transaction date'), 190 | ), 191 | migrations.AlterField( 192 | model_name='transaction', 193 | name='transaction_type', 194 | field=models.CharField(choices=[('w', 'withdrawal'), ('d', 'deposit')], max_length=1, verbose_name='Transaction type'), 195 | ), 196 | migrations.AlterField( 197 | model_name='transaction', 198 | name='value_gross', 199 | field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Value gross'), 200 | ), 201 | migrations.AlterField( 202 | model_name='transaction', 203 | name='value_net', 204 | field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Value net'), 205 | ), 206 | migrations.AlterField( 207 | model_name='transaction', 208 | name='vat', 209 | field=models.DecimalField(decimal_places=2, default=0, max_digits=4, verbose_name='VAT'), 210 | ), 211 | ] 212 | -------------------------------------------------------------------------------- /account_keeping/south_migrations/0005_move_rates_to_history_app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from south.utils import datetime_utils as datetime 3 | from south.db import db 4 | from south.v2 import DataMigration 5 | from django.conf import settings 6 | from django.db import models 7 | 8 | class Migration(DataMigration): 9 | 10 | def forwards(self, orm): 11 | for currency in orm.Currency.objects.all(): 12 | orm['currency_history.Currency'].objects.get_or_create( 13 | title=currency.name, 14 | iso_code=currency.iso_code, 15 | ) 16 | for rate in orm.CurrencyRate.objects.all(): 17 | from_currency = orm['currency_history.Currency'].objects.get( 18 | iso_code=rate.currency.iso_code) 19 | to_currency = orm['currency_history.Currency'].objects.get( 20 | iso_code=getattr(settings, 'BASE_CURRENCY', 'EUR')) 21 | rate_obj, created = orm[ 22 | 'currency_history.CurrencyRate'].objects.get_or_create( 23 | from_currency=from_currency, 24 | to_currency=to_currency, 25 | ) 26 | history = orm['currency_history.CurrencyRateHistory'].objects.create( 27 | value=rate.rate, 28 | rate=rate_obj, 29 | ) 30 | history.date = history.date.replace(day=1).replace( 31 | month=rate.month).replace(year=rate.year) 32 | history.save() 33 | 34 | def backwards(self, orm): 35 | "Write your backwards methods here." 36 | 37 | models = { 38 | u'account_keeping.account': { 39 | 'Meta': {'object_name': 'Account'}, 40 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'accounts'", 'to': u"orm['currency_history.Currency']"}), 41 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'initial_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 43 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 44 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '128'}), 45 | 'total_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}) 46 | }, 47 | u'account_keeping.category': { 48 | 'Meta': {'ordering': "['name']", 'object_name': 'Category'}, 49 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 50 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}) 51 | }, 52 | u'account_keeping.currency': { 53 | 'Meta': {'object_name': 'Currency'}, 54 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'is_base_currency': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 56 | 'iso_code': ('django.db.models.fields.CharField', [], {'max_length': '3'}), 57 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}) 58 | }, 59 | u'account_keeping.currencyrate': { 60 | 'Meta': {'object_name': 'CurrencyRate'}, 61 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['account_keeping.Currency']"}), 62 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 63 | 'month': ('django.db.models.fields.PositiveIntegerField', [], {}), 64 | 'rate': ('django.db.models.fields.DecimalField', [], {'max_digits': '18', 'decimal_places': '8'}), 65 | 'year': ('django.db.models.fields.PositiveIntegerField', [], {}) 66 | }, 67 | u'account_keeping.invoice': { 68 | 'Meta': {'ordering': "['-invoice_date']", 'object_name': 'Invoice'}, 69 | 'amount_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 70 | 'amount_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 71 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invoices'", 'to': u"orm['currency_history.Currency']"}), 72 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 73 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'invoice_date': ('django.db.models.fields.DateField', [], {}), 75 | 'invoice_number': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), 76 | 'invoice_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 77 | 'payment_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), 78 | 'pdf': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 79 | 'value_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 80 | 'value_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 81 | 'vat': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '4', 'decimal_places': '2'}) 82 | }, 83 | u'account_keeping.payee': { 84 | 'Meta': {'ordering': "['name']", 'object_name': 'Payee'}, 85 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 86 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}) 87 | }, 88 | u'account_keeping.transaction': { 89 | 'Meta': {'ordering': "['-transaction_date']", 'object_name': 'Transaction'}, 90 | 'account': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Account']"}), 91 | 'amount_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 92 | 'amount_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 93 | 'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Category']"}), 94 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['currency_history.Currency']"}), 95 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 96 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 97 | 'invoice': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'transactions'", 'null': 'True', 'to': u"orm['account_keeping.Invoice']"}), 98 | 'invoice_number': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), 99 | 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['account_keeping.Transaction']"}), 100 | 'payee': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Payee']"}), 101 | 'transaction_date': ('django.db.models.fields.DateField', [], {}), 102 | 'transaction_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 103 | 'value_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 104 | 'value_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 105 | 'vat': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '4', 'decimal_places': '2'}) 106 | }, 107 | u'currency_history.currency': { 108 | 'Meta': {'ordering': "['iso_code']", 'object_name': 'Currency'}, 109 | 'abbreviation': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'}), 110 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 111 | 'iso_code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '3'}), 112 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 113 | }, 114 | u'currency_history.currencyrate': { 115 | 'Meta': {'ordering': "['from_currency__iso_code', 'to_currency__iso_code']", 'object_name': 'CurrencyRate'}, 116 | 'from_currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'rates_from'", 'to': u"orm['currency_history.Currency']"}), 117 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 118 | 'to_currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'rates_to'", 'to': u"orm['currency_history.Currency']"}) 119 | }, 120 | u'currency_history.currencyratehistory': { 121 | 'Meta': {'ordering': "['-date', 'rate__to_currency__iso_code']", 'object_name': 'CurrencyRateHistory'}, 122 | 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 123 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 124 | 'rate': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history'", 'to': u"orm['currency_history.CurrencyRate']"}), 125 | 'tracked_by': ('django.db.models.fields.CharField', [], {'default': "u'Add your email'", 'max_length': '512'}), 126 | 'value': ('django.db.models.fields.FloatField', [], {}) 127 | } 128 | } 129 | 130 | complete_apps = ['account_keeping'] 131 | symmetrical = True 132 | -------------------------------------------------------------------------------- /account_keeping/tests/views_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the views of the account_keeping app.""" 2 | import json 3 | 4 | from django.core.urlresolvers import reverse 5 | from django.test import TestCase 6 | from django.utils.timezone import now, timedelta 7 | 8 | from django_libs.tests.mixins import ViewRequestFactoryTestMixin 9 | from mixer.backend.django import mixer 10 | from mock import patch 11 | from requests import Response 12 | 13 | from .. import views 14 | 15 | 16 | class AllTimeViewTestCase(ViewRequestFactoryTestMixin, TestCase): 17 | """Tests for the ``AllTimeView`` view class.""" 18 | view_class = views.AllTimeView 19 | 20 | def setUp(self): 21 | self.user = mixer.blend('auth.User', is_superuser=True) 22 | self.ccy = mixer.blend('currency_history.Currency', iso_code='EUR') 23 | self.account = mixer.blend( 24 | 'account_keeping.Account', currency=self.ccy, 25 | branch__currency=self.ccy) 26 | self.trans1 = mixer.blend( 27 | 'account_keeping.Transaction', 28 | account=self.account, currency=self.ccy) 29 | 30 | self.ccy2 = mixer.blend('currency_history.Currency') 31 | self.account2 = mixer.blend( 32 | 'account_keeping.Account', currency=self.ccy2, 33 | branch__currency=self.ccy2) 34 | self.trans2 = mixer.blend( 35 | 'account_keeping.Transaction', 36 | account=self.account2, currency=self.ccy2) 37 | mixer.blend( 38 | 'currency_history.CurrencyRateHistory', 39 | rate__from_currency=self.ccy2, 40 | rate__to_currency=self.ccy, 41 | date=now(), 42 | ) 43 | 44 | def test_view(self): 45 | self.should_redirect_to_login_when_anonymous() 46 | req = self.get_get_request(user=self.user) 47 | req.COOKIES['django_account_keeping_branch'] = 'main' 48 | view = self.get_view() 49 | resp = view(req) 50 | self.assert200(resp, self.user) 51 | 52 | 53 | class CurrentMonthRedirectViewTestCase(ViewRequestFactoryTestMixin, TestCase): 54 | """Tests for the ``CurrentMonthRedirectView`` view class.""" 55 | view_class = views.CurrentMonthRedirectView 56 | 57 | def setUp(self): 58 | super(CurrentMonthRedirectViewTestCase, self).setUp() 59 | self.now_ = now() 60 | 61 | def get_view_kwargs(self): 62 | return {'year': self.now_.year, 'month': self.now_.month} 63 | 64 | def test_view(self): 65 | expected_url = reverse( 66 | 'account_keeping_month', kwargs=self.get_view_kwargs()) 67 | self.redirects(expected_url) 68 | 69 | 70 | class CurrentYearRedirectViewTestCase(ViewRequestFactoryTestMixin, TestCase): 71 | """Tests for the ``CurrentYearRedirectView`` view class.""" 72 | view_class = views.CurrentYearRedirectView 73 | 74 | def setUp(self): 75 | super(CurrentYearRedirectViewTestCase, self).setUp() 76 | self.now_ = now() 77 | 78 | def get_view_kwargs(self): 79 | return {'year': self.now_.year, } 80 | 81 | def test_view(self): 82 | expected_url = reverse( 83 | 'account_keeping_year', kwargs=self.get_view_kwargs()) 84 | self.redirects(expected_url) 85 | 86 | 87 | class MonthViewTestCase(ViewRequestFactoryTestMixin, TestCase): 88 | """Tests for the ``MonthView`` view class.""" 89 | view_class = views.MonthView 90 | 91 | def setUp(self): 92 | self.user = mixer.blend('auth.User', is_superuser=True) 93 | self.ccy = mixer.blend('currency_history.Currency', iso_code='EUR') 94 | self.account = mixer.blend( 95 | 'account_keeping.Account', currency=self.ccy, 96 | branch__currency=self.ccy) 97 | self.trans1 = mixer.blend( 98 | 'account_keeping.Transaction', 99 | account=self.account, currency=self.ccy) 100 | 101 | self.ccy2 = mixer.blend('currency_history.Currency') 102 | self.account2 = mixer.blend( 103 | 'account_keeping.Account', currency=self.ccy2, 104 | branch__currency=self.ccy2) 105 | self.trans2 = mixer.blend( 106 | 'account_keeping.Transaction', 107 | account=self.account2, currency=self.ccy2) 108 | rate = mixer.blend( 109 | 'currency_history.CurrencyRateHistory', 110 | rate__from_currency=self.ccy2, 111 | rate__to_currency=self.ccy, 112 | ) 113 | rate.date = now() - timedelta(days=131) 114 | rate.save() 115 | 116 | def get_view_kwargs(self): 117 | return { 118 | 'year': self.trans1.transaction_date.year, 119 | 'month': self.trans1.transaction_date.month, 120 | } 121 | 122 | def test_view(self): 123 | self.should_redirect_to_login_when_anonymous() 124 | req = self.get_get_request(user=self.user, 125 | view_kwargs=self.get_view_kwargs()) 126 | req.COOKIES['django_account_keeping_branch'] = 'foo' 127 | view = self.get_view() 128 | resp = view(req, **self.get_view_kwargs()) 129 | self.assert200(resp, self.user) 130 | self.is_callable(self.user, kwargs={ 131 | 'year': now().year, 'month': now().month}) 132 | 133 | 134 | class YearOverviewViewTestCase(ViewRequestFactoryTestMixin, TestCase): 135 | """Tests for the ``YearOverviewView`` view class.""" 136 | view_class = views.YearOverviewView 137 | 138 | def setUp(self): 139 | self.user = mixer.blend('auth.User', is_superuser=True) 140 | self.ccy = mixer.blend('currency_history.Currency', iso_code='EUR') 141 | self.account = mixer.blend( 142 | 'account_keeping.Account', currency=self.ccy, 143 | branch__currency=self.ccy) 144 | self.trans1 = mixer.blend( 145 | 'account_keeping.Transaction', transaction_date=now(), 146 | account=self.account, currency=self.ccy) 147 | mixer.blend( 148 | 'account_keeping.Transaction', transaction_date=now(), 149 | account=self.account, currency=self.ccy, 150 | transaction_type=views.DEPOSIT) 151 | 152 | self.ccy2 = mixer.blend('currency_history.Currency') 153 | for i in range(11): 154 | new_date = now().replace(day=1).replace(month=i + 2) 155 | rate = mixer.blend( 156 | 'currency_history.CurrencyRateHistory', 157 | rate__from_currency=self.ccy2, 158 | rate__to_currency=self.ccy, 159 | ) 160 | rate.date = new_date 161 | rate.save() 162 | self.account2 = mixer.blend( 163 | 'account_keeping.Account', currency=self.ccy2, 164 | branch__currency=self.ccy2) 165 | mixer.blend( 166 | 'account_keeping.Transaction', transaction_date=now(), 167 | transaction_type=views.WITHDRAWAL, 168 | account=self.account2, currency=self.ccy2) 169 | mixer.blend( 170 | 'account_keeping.Transaction', transaction_date=now(), 171 | transaction_type=views.DEPOSIT, 172 | account=self.account2, currency=self.ccy) 173 | 174 | mixer.blend( 175 | 'account_keeping.Invoice', invoice_date=now(), 176 | invoice_type=views.DEPOSIT, 177 | account=self.account2, currency=self.ccy2, 178 | branch__currency=self.ccy2) 179 | 180 | def get_view_kwargs(self): 181 | return {'year': now().year, } 182 | 183 | def test_view(self): 184 | self.should_redirect_to_login_when_anonymous() 185 | self.is_callable(self.user) 186 | self.is_callable(self.user, kwargs={'year': now().year}) 187 | 188 | 189 | class IndexViewTestCase(ViewRequestFactoryTestMixin, TestCase): 190 | """Tests for the ``IndexView`` view class.""" 191 | view_class = views.IndexView 192 | 193 | def setUp(self): 194 | self.user = mixer.blend('auth.User', is_superuser=True) 195 | mixer.blend('account_keeping.Transaction') 196 | 197 | @patch('requests.request') 198 | def test_view(self, mock): 199 | resp = Response() 200 | resp.status_code = 200 201 | resp._content = json.dumps([{ 202 | 'reference': 1, 203 | }]) 204 | mock.return_value = resp 205 | self.should_redirect_to_login_when_anonymous() 206 | self.is_callable(self.user) 207 | 208 | 209 | class BranchSelectViewTestCase(ViewRequestFactoryTestMixin, TestCase): 210 | """Tests for the ``BranchSelectView`` view class.""" 211 | view_class = views.BranchSelectView 212 | 213 | def setUp(self): 214 | self.user = mixer.blend('auth.User') 215 | self.branch = mixer.blend('account_keeping.Branch') 216 | 217 | def get_view_kwargs(self): 218 | return {'slug': self.branch.slug} 219 | 220 | def test_view(self): 221 | self.redirects(user=self.user, to='/') 222 | self.is_not_callable(self.user, kwargs={'slug': 'foo'}) 223 | 224 | 225 | class PayeeListViewTestCase(ViewRequestFactoryTestMixin, TestCase): 226 | """Tests for the ``PayeeListView`` view class.""" 227 | view_class = views.PayeeListView 228 | 229 | def setUp(self): 230 | self.user = mixer.blend('auth.User', is_superuser=True) 231 | mixer.blend('account_keeping.Transaction') 232 | 233 | def test_view(self): 234 | self.should_redirect_to_login_when_anonymous() 235 | self.is_callable(self.user) 236 | 237 | 238 | class InvoiceCreateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 239 | """Tests for the ``InvoiceCreateView`` view class.""" 240 | view_class = views.InvoiceCreateView 241 | 242 | def setUp(self): 243 | self.user = mixer.blend('auth.User', is_superuser=True) 244 | 245 | def test_view(self): 246 | self.is_callable(self.user) 247 | self.is_postable(self.user, data={ 248 | 'invoice_type': 'd', 249 | 'invoice_date': now().strftime('%Y-%m-%d'), 250 | 'currency': mixer.blend('currency_history.Currency').pk, 251 | 'branch': mixer.blend('account_keeping.Branch').pk, 252 | 'amount_net': 0, 253 | 'amount_gross': 0, 254 | 'vat': 0, 255 | 'value_net': 0, 256 | 'value_gross': 0, 257 | }, to_url_name='account_keeping_month') 258 | 259 | 260 | class TransactionCreateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 261 | """Tests for the ``TransactionCreateView`` view class.""" 262 | view_class = views.TransactionCreateView 263 | 264 | def setUp(self): 265 | self.user = mixer.blend('auth.User', is_superuser=True) 266 | 267 | def test_view(self): 268 | self.is_callable(self.user) 269 | date = now() 270 | account = mixer.blend('account_keeping.Account') 271 | data = { 272 | 'transaction_type': 'd', 273 | 'transaction_date': date.strftime('%Y-%m-%d'), 274 | 'account': account.pk, 275 | 'payee': mixer.blend('account_keeping.Payee').pk, 276 | 'category': mixer.blend('account_keeping.Category').pk, 277 | 'currency': mixer.blend('currency_history.Currency').pk, 278 | 'amount_net': 0, 279 | 'amount_gross': 0, 280 | 'vat': 0, 281 | 'value_net': 0, 282 | 'value_gross': 0, 283 | } 284 | self.is_postable(self.user, data=data, to=u'{}#{}'.format( 285 | reverse('account_keeping_month', kwargs={ 286 | 'year': date.year, 'month': date.month}), account.slug)) 287 | 288 | self.is_callable(self.user, data={'invoice': 1}) 289 | self.is_callable(self.user, data={'parent': 1}) 290 | 291 | 292 | class TransactionExportViewTestCase(ViewRequestFactoryTestMixin, TestCase): 293 | """Tests for the ``TransactionExportView`` view class.""" 294 | view_class = views.TransactionExportView 295 | 296 | def setUp(self): 297 | self.user = mixer.blend('auth.User', is_superuser=True) 298 | 299 | def test_view(self): 300 | self.should_redirect_to_login_when_anonymous() 301 | self.is_callable(self.user) 302 | self.is_postable(user=self.user, ajax=True, data={ 303 | 'account': mixer.blend('account_keeping.Account').pk, 304 | 'start': '2015-01-01', 305 | 'end': '2018-01-01', 306 | }) 307 | -------------------------------------------------------------------------------- /account_keeping/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | from south.utils import datetime_utils as 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 'Currency' 13 | db.create_table(u'account_keeping_currency', ( 14 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 15 | ('name', self.gf('django.db.models.fields.CharField')(max_length=64)), 16 | ('iso_code', self.gf('django.db.models.fields.CharField')(max_length=3)), 17 | ('is_base_currency', self.gf('django.db.models.fields.BooleanField')(default=False)), 18 | )) 19 | db.send_create_signal(u'account_keeping', ['Currency']) 20 | 21 | # Adding model 'CurrencyRate' 22 | db.create_table(u'account_keeping_currencyrate', ( 23 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 24 | ('currency', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['account_keeping.Currency'])), 25 | ('year', self.gf('django.db.models.fields.PositiveIntegerField')()), 26 | ('month', self.gf('django.db.models.fields.PositiveIntegerField')()), 27 | ('rate', self.gf('django.db.models.fields.DecimalField')(max_digits=18, decimal_places=8)), 28 | )) 29 | db.send_create_signal(u'account_keeping', ['CurrencyRate']) 30 | 31 | # Adding model 'Account' 32 | db.create_table(u'account_keeping_account', ( 33 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 34 | ('name', self.gf('django.db.models.fields.CharField')(max_length=128)), 35 | ('slug', self.gf('django.db.models.fields.SlugField')(max_length=128)), 36 | ('currency', self.gf('django.db.models.fields.related.ForeignKey')(related_name='accounts', to=orm['account_keeping.Currency'])), 37 | ('initial_amount', self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=10, decimal_places=2)), 38 | ('total_amount', self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=10, decimal_places=2)), 39 | )) 40 | db.send_create_signal(u'account_keeping', ['Account']) 41 | 42 | # Adding model 'Invoice' 43 | db.create_table(u'account_keeping_invoice', ( 44 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 45 | ('invoice_type', self.gf('django.db.models.fields.CharField')(max_length=1)), 46 | ('invoice_date', self.gf('django.db.models.fields.DateField')()), 47 | ('invoice_number', self.gf('django.db.models.fields.CharField')(max_length=256, blank=True)), 48 | ('currency', self.gf('django.db.models.fields.related.ForeignKey')(related_name='invoices', to=orm['account_keeping.Currency'])), 49 | ('amount_net', self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=10, decimal_places=2)), 50 | ('vat', self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=4, decimal_places=2)), 51 | ('amount_gross', self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=10, decimal_places=2)), 52 | ('payment_date', self.gf('django.db.models.fields.DateField')(null=True, blank=True)), 53 | ('pdf', self.gf('django.db.models.fields.files.FileField')(max_length=100, null=True, blank=True)), 54 | )) 55 | db.send_create_signal(u'account_keeping', ['Invoice']) 56 | 57 | # Adding model 'Payee' 58 | db.create_table(u'account_keeping_payee', ( 59 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 60 | ('name', self.gf('django.db.models.fields.CharField')(max_length=256)), 61 | )) 62 | db.send_create_signal(u'account_keeping', ['Payee']) 63 | 64 | # Adding model 'Category' 65 | db.create_table(u'account_keeping_category', ( 66 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 67 | ('name', self.gf('django.db.models.fields.CharField')(max_length=256)), 68 | )) 69 | db.send_create_signal(u'account_keeping', ['Category']) 70 | 71 | # Adding model 'Transaction' 72 | db.create_table(u'account_keeping_transaction', ( 73 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 74 | ('account', self.gf('django.db.models.fields.related.ForeignKey')(related_name='transactions', to=orm['account_keeping.Account'])), 75 | ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='children', null=True, to=orm['account_keeping.Transaction'])), 76 | ('transaction_type', self.gf('django.db.models.fields.CharField')(max_length=1)), 77 | ('transaction_date', self.gf('django.db.models.fields.DateField')()), 78 | ('description', self.gf('django.db.models.fields.TextField')(blank=True)), 79 | ('invoice_number', self.gf('django.db.models.fields.CharField')(max_length=256, blank=True)), 80 | ('invoice', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='transactions', null=True, to=orm['account_keeping.Invoice'])), 81 | ('payee', self.gf('django.db.models.fields.related.ForeignKey')(related_name='transactions', to=orm['account_keeping.Payee'])), 82 | ('category', self.gf('django.db.models.fields.related.ForeignKey')(related_name='transactions', to=orm['account_keeping.Category'])), 83 | ('currency', self.gf('django.db.models.fields.related.ForeignKey')(related_name='transactions', to=orm['account_keeping.Currency'])), 84 | ('amount_net', self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=10, decimal_places=2, blank=True)), 85 | ('vat', self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=4, decimal_places=2)), 86 | ('amount_gross', self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=10, decimal_places=2, blank=True)), 87 | ('value_net', self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=10, decimal_places=2)), 88 | ('value_gross', self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=10, decimal_places=2)), 89 | )) 90 | db.send_create_signal(u'account_keeping', ['Transaction']) 91 | 92 | 93 | def backwards(self, orm): 94 | # Deleting model 'Currency' 95 | db.delete_table(u'account_keeping_currency') 96 | 97 | # Deleting model 'CurrencyRate' 98 | db.delete_table(u'account_keeping_currencyrate') 99 | 100 | # Deleting model 'Account' 101 | db.delete_table(u'account_keeping_account') 102 | 103 | # Deleting model 'Invoice' 104 | db.delete_table(u'account_keeping_invoice') 105 | 106 | # Deleting model 'Payee' 107 | db.delete_table(u'account_keeping_payee') 108 | 109 | # Deleting model 'Category' 110 | db.delete_table(u'account_keeping_category') 111 | 112 | # Deleting model 'Transaction' 113 | db.delete_table(u'account_keeping_transaction') 114 | 115 | 116 | models = { 117 | u'account_keeping.account': { 118 | 'Meta': {'object_name': 'Account'}, 119 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'accounts'", 'to': u"orm['account_keeping.Currency']"}), 120 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 121 | 'initial_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 122 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 123 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '128'}), 124 | 'total_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}) 125 | }, 126 | u'account_keeping.category': { 127 | 'Meta': {'object_name': 'Category'}, 128 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 129 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}) 130 | }, 131 | u'account_keeping.currency': { 132 | 'Meta': {'object_name': 'Currency'}, 133 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 134 | 'is_base_currency': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 135 | 'iso_code': ('django.db.models.fields.CharField', [], {'max_length': '3'}), 136 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}) 137 | }, 138 | u'account_keeping.currencyrate': { 139 | 'Meta': {'object_name': 'CurrencyRate'}, 140 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['account_keeping.Currency']"}), 141 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 142 | 'month': ('django.db.models.fields.PositiveIntegerField', [], {}), 143 | 'rate': ('django.db.models.fields.DecimalField', [], {'max_digits': '18', 'decimal_places': '8'}), 144 | 'year': ('django.db.models.fields.PositiveIntegerField', [], {}) 145 | }, 146 | u'account_keeping.invoice': { 147 | 'Meta': {'ordering': "['invoice_date']", 'object_name': 'Invoice'}, 148 | 'amount_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 149 | 'amount_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 150 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invoices'", 'to': u"orm['account_keeping.Currency']"}), 151 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 152 | 'invoice_date': ('django.db.models.fields.DateField', [], {}), 153 | 'invoice_number': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), 154 | 'invoice_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 155 | 'payment_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), 156 | 'pdf': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 157 | 'vat': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '4', 'decimal_places': '2'}) 158 | }, 159 | u'account_keeping.payee': { 160 | 'Meta': {'object_name': 'Payee'}, 161 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 162 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}) 163 | }, 164 | u'account_keeping.transaction': { 165 | 'Meta': {'ordering': "['transaction_date']", 'object_name': 'Transaction'}, 166 | 'account': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Account']"}), 167 | 'amount_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 168 | 'amount_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), 169 | 'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Category']"}), 170 | 'currency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Currency']"}), 171 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 172 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 173 | 'invoice': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'transactions'", 'null': 'True', 'to': u"orm['account_keeping.Invoice']"}), 174 | 'invoice_number': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), 175 | 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['account_keeping.Transaction']"}), 176 | 'payee': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transactions'", 'to': u"orm['account_keeping.Payee']"}), 177 | 'transaction_date': ('django.db.models.fields.DateField', [], {}), 178 | 'transaction_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), 179 | 'value_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 180 | 'value_net': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '10', 'decimal_places': '2'}), 181 | 'vat': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '4', 'decimal_places': '2'}) 182 | } 183 | } 184 | 185 | complete_apps = ['account_keeping'] 186 | -------------------------------------------------------------------------------- /account_keeping/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Models for the account_keeping app. 3 | 4 | TODO: Add lazy_trans and docstrings 5 | 6 | """ 7 | from datetime import date 8 | from decimal import Decimal 9 | 10 | from django.db import models 11 | from django.utils.encoding import python_2_unicode_compatible 12 | from django.utils.translation import ugettext_lazy as _ 13 | 14 | from currency_history.models import Currency, CurrencyRateHistory 15 | from dateutil import relativedelta 16 | from import_export import resources 17 | from import_export.fields import Field 18 | 19 | 20 | class AmountMixin(object): 21 | """ 22 | Mixin that handles amount_net, vat and amount_gross fields on save(). 23 | 24 | """ 25 | def set_amount_fields(self): 26 | if self.amount_net and not self.amount_gross: 27 | if self.vat: 28 | self.amount_gross = round( 29 | self.amount_net * (self.vat / Decimal(100.0) + 1), 2) 30 | else: 31 | self.amount_gross = self.amount_net 32 | 33 | if self.amount_gross and not self.amount_net: 34 | if self.vat: 35 | self.amount_net = round( 36 | Decimal(1.0) / ( 37 | self.vat / Decimal(100.0) + 1) * self.amount_gross, 2) 38 | else: 39 | self.amount_net = self.amount_gross 40 | 41 | def set_value_fields(self, type_field_name): 42 | multiplier = 1 43 | type_ = getattr(self, type_field_name) 44 | if type_ == Transaction.TRANSACTION_TYPES['withdrawal']: 45 | multiplier = -1 46 | self.value_net = self.amount_net * multiplier 47 | self.value_gross = self.amount_gross * multiplier 48 | 49 | 50 | @python_2_unicode_compatible 51 | class Branch(models.Model): 52 | name = models.CharField( 53 | max_length=128, 54 | verbose_name=_('Name'), 55 | ) 56 | 57 | slug = models.SlugField( 58 | max_length=128, 59 | verbose_name=_('Slug'), 60 | unique=True, 61 | ) 62 | 63 | currency = models.ForeignKey( 64 | 'currency_history.Currency', 65 | related_name='branches', 66 | verbose_name=_('Currency'), 67 | ) 68 | 69 | def __str__(self): 70 | return self.name 71 | 72 | class Meta: 73 | ordering = ['name'] 74 | verbose_name = _('Branch') 75 | verbose_name_plural = _('Branches') 76 | 77 | 78 | @python_2_unicode_compatible 79 | class Account(models.Model): 80 | name = models.CharField( 81 | max_length=128, 82 | verbose_name=_('Name'), 83 | ) 84 | 85 | slug = models.SlugField( 86 | max_length=128, 87 | verbose_name=_('Slug'), 88 | ) 89 | 90 | branch = models.ForeignKey( 91 | 'account_keeping.Branch', 92 | related_name='accounts', 93 | verbose_name=_('Branch'), 94 | ) 95 | 96 | currency = models.ForeignKey( 97 | 'currency_history.Currency', 98 | related_name='accounts', 99 | verbose_name=_('Currency'), 100 | ) 101 | 102 | initial_amount = models.DecimalField( 103 | max_digits=18, 104 | decimal_places=2, 105 | default=0, 106 | verbose_name=_('Initial amount'), 107 | ) 108 | 109 | total_amount = models.DecimalField( 110 | max_digits=18, 111 | decimal_places=2, 112 | default=0, 113 | verbose_name=_('Total amount'), 114 | ) 115 | 116 | active = models.BooleanField( 117 | default=True, 118 | verbose_name=_('Active?'), 119 | ) 120 | 121 | def __str__(self): 122 | return self.name 123 | 124 | class Meta: 125 | ordering = ['name'] 126 | verbose_name = _('Account') 127 | verbose_name_plural = _('Accounts') 128 | 129 | def get_balance(self, month=None): 130 | """ 131 | Returns the balance up until now or until the provided month. 132 | 133 | """ 134 | if not month: 135 | month = date(date.today().year, date.today().month, 1) 136 | next_month = month + relativedelta.relativedelta(months=1) 137 | account_balance = self.transactions.filter( 138 | parent__isnull=True, 139 | transaction_date__lt=next_month, 140 | ).aggregate(models.Sum('value_gross'))['value_gross__sum'] or 0 141 | account_balance = account_balance + self.initial_amount 142 | return account_balance 143 | 144 | 145 | class InvoiceManager(models.Manager): 146 | """Custom manager for the ``Invoice`` model.""" 147 | def get_without_pdf(self): 148 | qs = Invoice.objects.filter(pdf='') 149 | qs = qs.prefetch_related('transactions', ) 150 | return qs 151 | 152 | 153 | @python_2_unicode_compatible 154 | class Invoice(AmountMixin, models.Model): 155 | INVOICE_TYPES = { 156 | 'withdrawal': 'w', 157 | 'deposit': 'd', 158 | } 159 | 160 | INVOICE_TYPE_CHOICES = [ 161 | (INVOICE_TYPES['withdrawal'], 'withdrawal'), 162 | (INVOICE_TYPES['deposit'], 'deposit'), 163 | ] 164 | 165 | branch = models.ForeignKey( 166 | 'account_keeping.Branch', 167 | related_name='invoices', 168 | verbose_name=_('Branch'), 169 | ) 170 | 171 | invoice_type = models.CharField( 172 | max_length=1, 173 | choices=INVOICE_TYPE_CHOICES, 174 | verbose_name=_('Invoice type'), 175 | ) 176 | 177 | invoice_date = models.DateField( 178 | verbose_name=_('Invoice date'), 179 | ) 180 | 181 | invoice_number = models.CharField( 182 | max_length=256, 183 | verbose_name=_('Invoice No.'), 184 | blank=True, 185 | ) 186 | 187 | description = models.TextField( 188 | verbose_name=_('Description'), 189 | blank=True, 190 | ) 191 | 192 | currency = models.ForeignKey( 193 | 'currency_history.Currency', 194 | related_name='invoices', 195 | verbose_name=_('Currency'), 196 | ) 197 | 198 | amount_net = models.DecimalField( 199 | max_digits=18, 200 | decimal_places=2, 201 | default=0, 202 | blank=True, 203 | verbose_name=_('Amount net'), 204 | ) 205 | 206 | vat = models.DecimalField( 207 | max_digits=14, 208 | decimal_places=0, 209 | default=0, 210 | verbose_name=_('VAT in %'), 211 | ) 212 | 213 | amount_gross = models.DecimalField( 214 | max_digits=18, 215 | decimal_places=2, 216 | default=0, 217 | blank=True, 218 | verbose_name=_('Amount gross'), 219 | ) 220 | 221 | value_net = models.DecimalField( 222 | max_digits=18, 223 | decimal_places=2, 224 | default=0, 225 | verbose_name=_('Value net'), 226 | ) 227 | 228 | value_gross = models.DecimalField( 229 | max_digits=18, 230 | decimal_places=2, 231 | default=0, 232 | verbose_name=_('Value gross'), 233 | ) 234 | 235 | payment_date = models.DateField( 236 | verbose_name=_('Payment date'), 237 | blank=True, 238 | null=True, 239 | ) 240 | 241 | pdf = models.FileField( 242 | upload_to='invoice_files', 243 | verbose_name=_('PDF'), 244 | blank=True, null=True, 245 | ) 246 | 247 | objects = InvoiceManager() 248 | 249 | class Meta: 250 | ordering = ['-invoice_date', '-pk'] 251 | verbose_name = _('Invoice') 252 | verbose_name_plural = _('Invoices') 253 | 254 | def __str__(self): 255 | if self.invoice_number: 256 | return self.invoice_number 257 | return '{0} - {1}'.format(self.invoice_date, 258 | self.get_invoice_type_display()) 259 | 260 | def save(self, *args, **kwargs): 261 | self.set_amount_fields() 262 | self.set_value_fields('invoice_type') 263 | return super(Invoice, self).save(*args, **kwargs) 264 | 265 | @property 266 | def balance(self): 267 | if not self.transactions.all(): 268 | return 0 - self.amount_net 269 | 270 | total = 0 271 | # Convert amounts 272 | for currency in Currency.objects.all(): 273 | # Get transactions for each currency 274 | transactions = self.transactions.filter(currency=currency) 275 | if not transactions: 276 | continue 277 | 278 | if currency == self.currency: 279 | rate = 1 280 | else: 281 | rate = Decimal(CurrencyRateHistory.objects.filter( 282 | rate__from_currency=currency, 283 | rate__to_currency=self.currency, 284 | )[0].value) 285 | total += rate * transactions.aggregate( 286 | models.Sum('amount_net'))['amount_net__sum'] 287 | return total - self.amount_net 288 | 289 | 290 | @python_2_unicode_compatible 291 | class Payee(models.Model): 292 | name = models.CharField( 293 | verbose_name=_('Payee'), 294 | max_length=256, 295 | ) 296 | 297 | class Meta: 298 | ordering = ['name', ] 299 | verbose_name = _('Payee') 300 | verbose_name_plural = _('Payees') 301 | 302 | def __str__(self): 303 | return self.name 304 | 305 | def invoices(self): 306 | return Invoice.objects.filter( 307 | pk__in=self.transactions.values_list('invoice__pk')).distinct() 308 | 309 | 310 | @python_2_unicode_compatible 311 | class Category(models.Model): 312 | name = models.CharField( 313 | max_length=256, 314 | verbose_name=_('Name'), 315 | ) 316 | 317 | class Meta: 318 | ordering = ['name', ] 319 | verbose_name = _('Category') 320 | verbose_name_plural = _('Categories') 321 | 322 | def __str__(self): 323 | return self.name 324 | 325 | 326 | class TransactionManager(models.Manager): 327 | """Manager for the ``Transaction`` model.""" 328 | def get_totals_by_payee(self, account, start_date=None, end_date=None): 329 | """ 330 | Returns transaction totals grouped by Payee. 331 | 332 | """ 333 | qs = Transaction.objects.filter(account=account, parent__isnull=True) 334 | qs = qs.values('payee').annotate(models.Sum('value_gross')) 335 | qs = qs.order_by('payee__name') 336 | return qs 337 | 338 | def get_without_invoice(self): 339 | """ 340 | Returns transactions that don't have an invoice. 341 | 342 | We filter out transactions that have children, because those 343 | transactions never have invoices - their children are the ones that 344 | would each have one invoice. 345 | 346 | """ 347 | qs = Transaction.objects.filter( 348 | children__isnull=True, invoice__isnull=True) 349 | return qs 350 | 351 | 352 | @python_2_unicode_compatible 353 | class Transaction(AmountMixin, models.Model): 354 | TRANSACTION_TYPES = { 355 | 'withdrawal': 'w', 356 | 'deposit': 'd', 357 | } 358 | 359 | TRANSACTION_TYPE_CHOICES = [ 360 | (TRANSACTION_TYPES['withdrawal'], 'withdrawal'), 361 | (TRANSACTION_TYPES['deposit'], 'deposit'), 362 | ] 363 | 364 | account = models.ForeignKey( 365 | Account, 366 | related_name='transactions', 367 | verbose_name=_('Account'), 368 | ) 369 | 370 | parent = models.ForeignKey( 371 | 'account_keeping.Transaction', 372 | related_name='children', 373 | blank=True, null=True, 374 | verbose_name=_('Parent'), 375 | ) 376 | 377 | transaction_type = models.CharField( 378 | max_length=1, 379 | choices=TRANSACTION_TYPE_CHOICES, 380 | verbose_name=_('Transaction type'), 381 | ) 382 | 383 | transaction_date = models.DateField( 384 | verbose_name=_('Transaction date'), 385 | ) 386 | 387 | description = models.TextField( 388 | verbose_name=_('Description'), 389 | blank=True, 390 | ) 391 | 392 | invoice_number = models.CharField( 393 | verbose_name=_('Invoice No.'), 394 | max_length=256, 395 | blank=True, 396 | ) 397 | 398 | invoice = models.ForeignKey( 399 | Invoice, 400 | blank=True, null=True, 401 | related_name='transactions', 402 | verbose_name=_('Invoice'), 403 | ) 404 | 405 | payee = models.ForeignKey( 406 | Payee, 407 | related_name='transactions', 408 | verbose_name=_('Payee'), 409 | ) 410 | 411 | category = models.ForeignKey( 412 | Category, 413 | related_name='transactions', 414 | verbose_name=_('Category'), 415 | ) 416 | 417 | currency = models.ForeignKey( 418 | 'currency_history.Currency', 419 | related_name='transactions', 420 | verbose_name=_('Currency'), 421 | ) 422 | 423 | amount_net = models.DecimalField( 424 | max_digits=18, 425 | decimal_places=2, 426 | default=0, 427 | blank=True, 428 | verbose_name=_('Amount net'), 429 | ) 430 | 431 | vat = models.DecimalField( 432 | max_digits=14, 433 | decimal_places=2, 434 | default=0, 435 | verbose_name=_('VAT'), 436 | ) 437 | 438 | amount_gross = models.DecimalField( 439 | max_digits=18, 440 | decimal_places=2, 441 | default=0, 442 | blank=True, 443 | verbose_name=_('Amount gross'), 444 | ) 445 | 446 | value_net = models.DecimalField( 447 | max_digits=18, 448 | decimal_places=2, 449 | default=0, 450 | verbose_name=_('Value net'), 451 | ) 452 | 453 | value_gross = models.DecimalField( 454 | max_digits=18, 455 | decimal_places=2, 456 | default=0, 457 | verbose_name=_('Value gross'), 458 | ) 459 | 460 | objects = TransactionManager() 461 | 462 | class Meta: 463 | ordering = ['-transaction_date', '-pk'] 464 | verbose_name = _('Transaction') 465 | verbose_name_plural = _('Transactions') 466 | 467 | def __str__(self): 468 | if self.invoice_number: 469 | return self.invoice_number 470 | if self.invoice and self.invoice.invoice_number: 471 | return self.invoice.invoice_number 472 | return '{0} - {1}'.format(self.payee, self.category) 473 | 474 | def get_description(self): 475 | if self.description: 476 | return self.description 477 | if self.invoice and self.invoice.description: 478 | return self.invoice.description 479 | description = '' 480 | for child in self.children.all(): 481 | if child.description: 482 | description += u'{0},\n'.format(child.description) 483 | elif child.invoice and child.invoice.description: 484 | description += u'{0},\n'.format(child.invoice.description) 485 | return description or u'n/a' 486 | 487 | def get_invoices(self): 488 | if self.children.all(): 489 | return [child.invoice for child in self.children.all()] 490 | return [self.invoice, ] 491 | 492 | def save(self, *args, **kwargs): 493 | self.set_amount_fields() 494 | self.set_value_fields('transaction_type') 495 | return super(Transaction, self).save(*args, **kwargs) 496 | 497 | 498 | class TransactionResource(resources.ModelResource): 499 | invoice_no = Field(column_name='Invoice No.') 500 | balance = Field(column_name='Balance') 501 | get_transaction_type = Field(column_name='Transaction type') 502 | 503 | @classmethod 504 | def field_from_django_field(self, field_name, django_field, readonly): 505 | field = resources.ModelResource.field_from_django_field( 506 | field_name, django_field, readonly) 507 | field.column_name = django_field.verbose_name 508 | return field 509 | 510 | class Meta: 511 | model = Transaction 512 | fields = ('id', 'transaction_date', 'description', 'invoice_no', 513 | 'payee__name', 'category__name', 'get_transaction_type', 514 | 'currency__iso_code', 'amount_net', 'vat', 'amount_gross', 515 | 'balance') 516 | export_order = fields 517 | 518 | def dehydrate_invoice_no(self, transaction): # pragma: nocover 519 | if transaction.invoice_number: 520 | return transaction.invoice_number 521 | if transaction.invoice and transaction.invoice.invoice_number: 522 | return transaction.invoice.invoice_number 523 | return '' 524 | 525 | def dehydrate_balance(self, transaction): # pragma: nocover 526 | account_balance = transaction.account.transactions.filter( 527 | models.Q(parent__isnull=True), 528 | models.Q(transaction_date__lt=transaction.transaction_date) | 529 | (models.Q(transaction_date=transaction.transaction_date, 530 | pk__lte=transaction.pk)), 531 | ).aggregate(models.Sum('value_gross'))['value_gross__sum'] or 0 532 | return account_balance + transaction.account.initial_amount 533 | 534 | def dehydrate_get_transaction_type(self, transaction): # pragma: nocover 535 | return transaction.get_transaction_type_display() 536 | --------------------------------------------------------------------------------