├── 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 |
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 |
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 |
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 |
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 | | {% trans "Payee" %} |
10 | {% trans "Invoices" %} |
11 | {% trans "Transactions" %} |
12 |
13 | {% for payee in object_list %}
14 |
15 | | {{ payee }} |
16 |
17 | {{ payee.invoices.count }}
18 |
19 |
20 |
21 | {% for invoice in payee.invoices.all %}
22 |
23 | |
24 | {{ invoice }}
25 | |
26 | {{ invoice.value_net|currency:invoice.currency }} |
27 |
28 | {% endfor %}
29 |
30 |
31 |
32 | |
33 |
34 | {{ payee.transactions.count }}
35 | |
36 |
37 | {% endfor %}
38 |
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 | | {% trans "Accounts" %} |
10 | {% trans "Balance" %} |
11 | {% trans "Transactions" %} |
12 |
13 | {% for account in object_list %}
14 |
15 | | {{ account.name }} |
16 | {{ account.get_balance|currency:account.currency }} |
17 |
18 | {{ account.transactions.count }}
19 |
20 |
21 |
22 | {% for transaction in account.transactions.all %}
23 |
24 | |
25 | {{ transaction }}
26 | |
27 | {{ transaction.transaction_date|date }} |
28 | {{ transaction.value_net|currency:transaction.currency }} |
29 |
30 | {% endfor %}
31 |
32 |
33 |
34 |
35 | |
36 |
37 | {% endfor %}
38 |
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 | | {% trans "ID" %} |
6 | {% trans "Date" %} |
7 | {% trans "Invoice number" %} |
8 | {% trans "PDF" %} |
9 | {% trans "Description" %} |
10 | {% trans "Currency" %} |
11 | {% trans "Amount (net)" %} |
12 | {% trans "VAT" %} |
13 | {% trans "Amount (gross)" %} |
14 | {% trans "Balance" %} |
15 | {% trans "Transactions" %} |
16 |
17 |
18 |
19 | {% for invoice in invoices %}
20 |
21 | | {{ invoice.pk }} |
22 | {{ invoice.invoice_date|date:"Y-m-d" }} |
23 | {{ invoice.invoice_number|default:"n/a" }} |
24 | {% if invoice.pdf %}{% else %}n/a{% endif %} |
25 | {{ invoice.description|default:"n/a" }} |
26 | {{ invoice.currency.iso_code }} |
27 | {{ invoice.amount_net|currency:invoice.currency }} |
28 | {{ invoice.vat|currency:invoice.currency }} |
29 | {{ invoice.amount_gross|currency:invoice.currency }} |
30 | {{ invoice.balance|currency:invoice.currency }} |
31 |
32 | {% for transaction in invoice.transactions.all %}
33 | {{ transaction.pk }}{% if not forloop.last %}, {% endif %}
34 | {% endfor %}
35 | |
36 |
37 | {% trans "Add transaction" %}
38 | |
39 |
40 | {% endfor %}
41 |
42 |
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 | | {% trans "Month" %} |
16 | {% trans "Income" %} |
17 | {% trans "Expenses" %} |
18 | {% trans "Profit" %} |
19 | {% trans "New invoiced income" %} |
20 | {% trans "Total outstanding profit" %} |
21 | {% trans "Bank balance" %} |
22 | {% trans "Total equity" %} |
23 |
24 |
25 |
26 | {% for month in months %}
27 |
28 | {% localtime off %}
29 | | {{ month|date:"M" }} |
30 | {% endlocaltime %}
31 | {% call income_total "get" month as income_total_value %}{{ income_total_value|default:0|currency:branch.currency }} |
32 | {% call expenses_total "get" month as expenses_total_value %}{{ expenses_total_value|default:0|currency:branch.currency }} |
33 | {% call profit_total "get" month as profit_total_value %}{{ profit_total_value|default:0|currency:branch.currency }} |
34 | {% call new_total "get" month as new_total_value %}{{ new_total_value|default:0|currency:branch.currency }} |
35 | {% call outstanding_total "get" month as outstanding_total_value %}{{ outstanding_total_value|default:0|currency:branch.currency }} |
36 | {% call balance_total "get" month as balance_total_value %}{{ balance_total_value|default:0|currency:branch.currency }} |
37 | {% call equity_total "get" month as equity_total_value %}{{ equity_total_value|default:0|currency:branch.currency }} |
38 |
39 | {% endfor %}
40 |
41 | | {% trans "Total" %} |
42 | {{ income_total_total|currency:branch.currency }} |
43 | {{ expenses_total_total|currency:branch.currency }} |
44 | {{ profit_total_total|currency:branch.currency }} |
45 | {{ new_total_total|currency:branch.currency }} |
46 | |
47 | |
48 | |
49 |
50 |
51 | | {% trans "Average" %} |
52 | {{ income_average|currency:branch.currency }} |
53 | {{ expenses_average|currency:branch.currency }} |
54 | {{ profit_average|currency:branch.currency }} |
55 | {{ new_average|currency:branch.currency }} |
56 | {{ outstanding_average|currency:branch.currency }} |
57 | {{ balance_average|currency}} |
58 | {{ equity_average|currency:branch.currency }} |
59 |
60 |
61 |
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 | {% trans "net" %} |
33 | {% trans "gross" %} |
34 |
35 |
36 |
37 |
38 |
39 | | {% trans "Income total" %} |
40 | {{ value_dict.income_net_total|currency:value_dict.account.currency }} |
41 | {{ value_dict.income_gross_total|currency:value_dict.account.currency }} |
42 |
43 |
44 | | {% trans "Expenses total" %} |
45 | {{ value_dict.expenses_net_total|currency:value_dict.account.currency }} |
46 | {{ value_dict.expenses_gross_total|currency:value_dict.account.currency }} |
47 |
48 |
49 | | {% trans "Profit total" %} |
50 | {{ value_dict.amount_net_total|currency:value_dict.account.currency }} |
51 | {{ value_dict.amount_gross_total|currency:value_dict.account.currency }} |
52 |
53 |
54 |
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 | | {% trans "Account" %} |
79 | {% trans "Expenses (net)" %} |
80 | {% trans "Expenses (gross)" %} |
81 | {% trans "Income (net)" %} |
82 | {% trans "Income (gross)" %} |
83 | {% trans "Profit (net)" %} |
84 | {% trans "Profit (gross)" %} |
85 |
86 |
87 |
88 | {% for value_dict in account_transactions %}
89 |
90 | | {{ value_dict.account.name }} |
91 | {{ value_dict.expenses_net_sum_base|currency:value_dict.account.currency }} |
92 | {{ value_dict.expenses_gross_sum_base|currency:value_dict.account.currency }} |
93 | {{ value_dict.income_net_sum_base|currency:value_dict.account.currency }} |
94 | {{ value_dict.income_gross_sum_base|currency:value_dict.account.currency }} |
95 | {{ value_dict.amount_net_sum_base|currency:value_dict.account.currency }} |
96 | {{ value_dict.amount_gross_sum_base|currency:value_dict.account.currency }} |
97 |
98 | {% endfor %}
99 |
100 | | {% trans "Total" %} |
101 | {{ totals.expenses_net|currency:branch.currency}} |
102 | {{ totals.expenses_gross|currency:branch.currency}} |
103 | {{ totals.income_net|currency:branch.currency}} |
104 | {{ totals.income_gross|currency:branch.currency}} |
105 | {{ totals.amount_net|currency:branch.currency}} |
106 | {{ totals.amount_gross|currency:branch.currency}} |
107 |
108 |
109 |
110 |
111 |
{% trans "Outstanding total" %} (CCY / {{ base_currency }})
112 |
113 |
114 |
115 |
116 | | {% trans "Currency" %} |
117 | {% trans "Expenses (gross)" %} |
118 | {% trans "Income (gross)" %} |
119 | {% trans "Profit (gross)" %} |
120 |
121 |
122 |
123 | {% for currency, ccy_totals in outstanding_ccy_totals.items %}
124 |
125 | | {{ currency.iso_code }} |
126 |
127 | {% include "account_keeping/partials/currency_base_pair.html" with amount=ccy_totals.expenses_gross base_amount=ccy_totals.expenses_gross_base %}
128 | |
129 |
130 | {% include "account_keeping/partials/currency_base_pair.html" with amount=ccy_totals.income_gross base_amount=ccy_totals.income_gross_base %}
131 | |
132 |
133 | {% include "account_keeping/partials/currency_base_pair.html" with amount=ccy_totals.profit_gross base_amount=ccy_totals.profit_gross_base %}
134 | |
135 |
136 | {% endfor %}
137 |
138 | | {% trans "Total" %} ({{ base_currency.iso_code }}) |
139 | {{ totals.outstanding_expenses_gross|currency:branch.currency}} |
140 | {{ totals.outstanding_income_gross|currency:branch.currency}} |
141 | {{ totals.outstanding_profit_gross|currency:branch.currency}} |
142 |
143 |
144 |
145 |
146 |
147 |
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 |
--------------------------------------------------------------------------------