Django: создание проекта "домашняя бухгалтерия"

Автор: | 13/05/2015

django_logo_2Для практики в Django – решил создать более продвинутую версию домашней бухгалтерии.

Старый вариант – консольный bash-скрипт, описан в посте bash + MySQL: скрипт домашней бухгалтерии.

Да и женщина никак не хочет приобщиться к прекрасному миру консольных приложений 🙂

Имеется сервер CentOS 6.6, Python 2.7, Django 1.8.

Доступ к проекту реализован через NGINX +uWSGI, база данных – MariaDB 5.

Файл настроек uWSGI:

[uwsgi]
socket = 127.0.0.1:9091
chdir = /var/www/django/money_domain_org_ua
pythonpath = /usr/local/lib/python2.7/site-packages/
module = django.core.wsgi:get_wsgi_application()
env = DJANGO_SETTINGS_MODULE=domain_money.settings
master = True
pidfile = /tmp/project-master.pid
daemonize=/var/log/uwsgi/money.domain.org.ua.log
plugins = python
uid = setevoy
gid = setevoy

NGINX:

server {
    server_name money.domain.org.ua;

    access_log /var/log/nginx/money.domain.org.ua-access.log;
    error_log /var/log/nginx/money.domain.org.ua-error.log;

    root /var/www/django/money_domain_org_ua/;

    auth_basic_user_file /var/www/django/money_domain_org_ua/.htaccess;
    auth_basic "Password-protected Area";

    location /static/ {
        alias /usr/local/lib/python2.7/site-packages/Django-1.8.1-py2.7.egg/django/contrib/admin/static/;
        expires modified +1w;
    }

    location /
    {

        index index.py;

        uwsgi_pass 127.0.0.1:9091;
        include uwsgi_params;
   }
}

Создание проекта и приложения

Переходим в нужный каталог и создаём приложение:

$ cd /var/www/django
$ django-admin startproject setevoy_money

Переименовываем корневой каталог проекта:

$ mv setevoy_money money_domain_org_ua
$ tree -L 2 money_domain_org_ua
money_domain_org_ua
├── manage.py
└── setevoy_money
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
$ cd money_domain_org_ua

Создаём базу:

MariaDB [(none)]> CREATE DATABASE setevoy_money CHARACTER SET utf8 COLLATE utf8_general_ci;
Query OK, 1 row affected (0.01 sec)
MariaDB [(none)]> GRANT ALL ON setevoy_money.* TO 'setevoy_money'@'%' IDENTIFIED BY 'p@ssw0rd';
Query OK, 0 rows affected (0.02 sec)

Драйвер MySQL для Python уже установлен. Если нет – можно установить через PIP:

# pip install MySQL-python

Настраиваем подключение. В файле setevoy_money/settings.py указываем:

...
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'setevoy_money',
        'USER': 'setevoy_money',
        'PASSWORD': 'p@ssw0rd',
        'HOST': 'localhost',
    }
}
...

Там же меняем часовой пояс:

...
TIME_ZONE = 'EET'
...

Запускаем, проверяем что Django работает:

$ python manage.py runserver 77.***.***.20:8000

Заполняем базу:

$ python manage.py migrate
Operations to perform:
  Synchronize unmigrated apps: staticfiles, messages
  Apply all migrations: admin, contenttypes, auth, sessions
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying sessions.0001_initial... OK

Создание моделей

Создаём приложение money, которое и будет выполнять всю работу:

$ python manage.py startapp money
$ tree -L 2 .
.
├── manage.py
├── money
│   ├── admin.py
│   ├── __init__.py
│   ├── migrations
│   ├── models.py
│   ├── tests.py
│   └── views.py
└── setevoy_money
 ├── __init__.py
 ├── __init__.pyc
 ├── settings.py
 ├── settings.pyc
 ├── urls.py
 ├── urls.pyc
 ├── wsgi.py
 └── wsgi.pyc

Примечание: в моделях ниже я сделал одну ошибку – согласно PEP8 имена классов дожны иметь вид ClassName, а не Class_name – как я написал. Поэтому – следует переписать Source_typesTransactions_types и Total_aval в SourceTypesTransactionsTypes и TotalAval соответственно.

В нём редактируем файл models.py:

# -*- coding: utf-8 -*-
from django.db import models

"""будет хранить общую доступную сумму
   на всех Source_types"""
class Total_aval(models.Model):
    avail_date = models.DateField()
    avail_sum = models.IntegerField()

    class Meta:
        verbose_name = 'Вcего доступно'

    def __unicode__(self):
        return self.avail_sum

""" у каждого юзера (члена семьи)
    свои источники платежей Source_types"""
class Users(models.Model):
    username = models.CharField(max_length=200)

    class Meta:
        verbose_name = 'Пользователи'

    def __unicode__(self):
        return self.username

"""источник денег - зарплатная карта,
   вебмани, наличные и т.д."""
class Source_types(models.Model):
    type = models.CharField(max_length=200)
    user = models.ForeignKey(Users, unique=False)
    source_sum = models.IntegerField()

    class Meta:
        verbose_name = 'Типы источников'

    def __unicode__(self):
        return self.type

"""типы транзакций - Приход/расход"""
class Transactions_types(models.Model):
    type = models.CharField(max_length=10)

    class Meta:
        verbose_name = 'Типы транзакций'

    def __unicode__(self):
        return self.type

"""самая важная таблица
   транзакции, которая будет содержать
   записи о получении денег, затрате денег
   тип транзакции - Приход/расход (Transactions_types)
   тип источника - карта, наличные и т.д. (Source_types)
   дата в формате Год-месяц-день
   сумма транзакции
   описание, например - "Купил XBox"""
class Transactions(models.Model):
    transaction_type = models.ForeignKey(Transactions_types)
    source_type = models.ForeignKey(Source_types)
    transaction_date = models.DateField()
    transaction_sum = models.IntegerField()
    transaction_desc = models.CharField(max_length=255)

    class Meta:
        verbose_name = 'Транзакции'

    def __unicode__(self):
        return self.transaction_desc

Возвращаемся к файлу setevoy_money/settings.py и добавляем новое приложение:

...
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'money',
)
...

Проверяем модели (хотя – это совсем не панацея, у меня нашлось несколько опечаток в именах таблиц ForeignKey при создании миграции):

$ python manage.py check
System check identified no issues (0 silenced).

Создаём миграцию:

$ python manage.py makemigrations money
Migrations for 'money':
  0001_initial.py:
    - Create model Source_types
    - Create model Total_aval
    - Create model Transactions
    - Create model Transactions_types
    - Create model Users
    - Add field transaction_type to transactions
    - Add field user to source_types

SQL будет выглядеть так:

 $ python manage.py sqlmigrate money 0001
BEGIN;
CREATE TABLE `money_source_types` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `type` varchar(200) NOT NULL, `source_sum` integer NOT NULL);
CREATE TABLE `money_total_aval` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `avail_date` date NOT NULL, `avail_sum` integer NOT NULL);
CREATE TABLE `money_transactions` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `transaction_date` date NOT NULL, `transaction_sum` integer NOT NULL, `transaction_desc` varchar(255) NOT NULL, `source_type_id` integer NOT NULL);
CREATE TABLE `money_transactions_types` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `type` varchar(10) NOT NULL);
CREATE TABLE `money_users` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(200) NOT NULL);
ALTER TABLE `money_transactions` ADD COLUMN `transaction_type_id` integer NOT NULL;
ALTER TABLE `money_transactions` ALTER COLUMN `transaction_type_id` DROP DEFAULT;
ALTER TABLE `money_source_types` ADD COLUMN `user_id` integer NOT NULL UNIQUE;
ALTER TABLE `money_source_types` ALTER COLUMN `user_id` DROP DEFAULT;
ALTER TABLE `money_transactions` ADD CONSTRAINT `money_tr_source_type_id_4459214f3675887_fk_money_source_types_id` FOREIGN KEY (`source_type_id`) REFERENCES `money_source_types` (`id`);
CREATE INDEX `money_transactions_92c431dc` ON `money_transactions` (`transaction_type_id`);
ALTER TABLE `money_transactions` ADD CONSTRAINT `da53091746f07a70e338cee4e66034bf` FOREIGN KEY (`transaction_type_id`) REFERENCES `money_transactions_types` (`id`);
ALTER TABLE `money_source_types` ADD CONSTRAINT `money_source_types_user_id_49efb61c4abcab27_fk_money_users_id` FOREIGN KEY (`user_id`) REFERENCES `money_users` (`id`);

COMMIT;

Создаём таблицы:

$ python manage.py migrate
Operations to perform:
  Synchronize unmigrated apps: staticfiles, messages
  Apply all migrations: admin, money, contenttypes, auth, sessions
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying money.0001_initial... OK

Вот как они выглядят в базе:

MariaDB [setevoy_money]> show tables where Tables_in_setevoy_money like 'money%';
+--------------------------+
| Tables_in_setevoy_money  |
+--------------------------+
| money_source_types       |
| money_total_aval         |
| money_transactions       |
| money_transactions_types |
| money_users              |
+--------------------------+
5 rows in set (0.00 sec)
MariaDB [setevoy_money]> desc money_source_types;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| type       | varchar(200) | NO   |     | NULL    |                |
| source_sum | int(11)      | NO   |     | NULL    |                |
| user_id    | int(11)      | NO   | MUL | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)
MariaDB [setevoy_money]> desc money_total_aval;
+------------+---------+------+-----+---------+----------------+
| Field      | Type    | Null | Key | Default | Extra          |
+------------+---------+------+-----+---------+----------------+
| id         | int(11) | NO   | PRI | NULL    | auto_increment |
| avail_date | date    | NO   |     | NULL    |                |
| avail_sum  | int(11) | NO   |     | NULL    |                |
+------------+---------+------+-----+---------+----------------+
3 rows in set (0.01 sec)
MariaDB [setevoy_money]> desc money_transactions;
+---------------------+--------------+------+-----+---------+----------------+
| Field               | Type         | Null | Key | Default | Extra          |
+---------------------+--------------+------+-----+---------+----------------+
| id                  | int(11)      | NO   | PRI | NULL    | auto_increment |
| transaction_date    | date         | NO   |     | NULL    |                |
| transaction_sum     | int(11)      | NO   |     | NULL    |                |
| transaction_desc    | varchar(255) | NO   |     | NULL    |                |
| source_type_id      | int(11)      | NO   | MUL | NULL    |                |
| transaction_type_id | int(11)      | NO   | MUL | NULL    |                |
+---------------------+--------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
MariaDB [setevoy_money]> desc money_transactions_types;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int(11)     | NO   | PRI | NULL    | auto_increment |
| type  | varchar(10) | NO   |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.01 sec)
MariaDB [setevoy_money]> desc money_users;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | int(11)      | NO   | PRI | NULL    | auto_increment |
| username | varchar(200) | NO   |     | NULL    |                |
+----------+--------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

Если что-то потребовалось поменять (например – я одно поле указал как OneToOneField, а не ForeignKey) – то редактируем класс модели, и создаём новую миграцию:

 $ python manage.py makemigrations money
Migrations for 'money':
  0002_auto_20150512_1204.py:
    - Change Meta options on source_types
    - Change Meta options on total_aval
    - Change Meta options on transactions
    - Change Meta options on transactions_types
    - Change Meta options on users
    - Alter field user on source_types
$ python manage.py sqlmigrate money 0002
BEGIN;
ALTER TABLE `money_source_types` DROP FOREIGN KEY `money_source_types_user_id_49efb61c4abcab27_fk_money_users_id`;
ALTER TABLE `money_source_types` DROP INDEX `user_id_2`;
ALTER TABLE `money_source_types` ADD CONSTRAINT `money_source_types_user_id_49efb61c4abcab27_fk_money_users_id` FOREIGN KEY (`user_id`) REFERENCES `money_users` (`id`);

COMMIT;
$ python manage.py migrate
Operations to perform:
  Synchronize unmigrated apps: staticfiles, messages
  Apply all migrations: admin, money, contenttypes, auth, sessions
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying money.0002_auto_20150512_1204... OK

Настройка админпанели

Создаём пользователя:

$ python manage.py createsuperuser
Username (leave blank to use 'setevoy'):
Email address: [email protected]
Password:
Password (again):
Superuser created successfully.

Редактируем файл money/admin.py, что бы добавить приложение money в админку:

# -*- coding: utf-8 -*-
from django.contrib import admin

from .models import Transactions, Transactions_types, Source_types, Users

class TransactionsAdmin(admin.ModelAdmin):
    list_display = ('transaction_desc', 'transaction_type', 'transaction_date', 'transaction_sum')

admin.site.register(Transactions, TransactionsAdmin)
admin.site.register(Transactions_types)
admin.site.register(Source_types)
admin.site.register(Users)

Перезапускаем uWSGI (хотя пока лучше было бы пользоваться runserver, конечно):

# service uwsgi restart
Shutting down uWSGI
Starting uWSGI                                             [  OK  ]

И проверяем:

django_25

Что бы продолжить работу – надо создать несколько записей:

money_!

пользователь (пока один)

money_2

тип источника (тоже пока один)

 

money_3

типы транзакций – два

Готово.

Переходим к представлениям.

Создание представлений и URLconf

Начнём с URLconf.

В файле setevoy_money/urls.py добавляем include():

# -*- coding: utf-8 -*-
from django.conf.urls import include, url
from django.contrib import admin
from money.views import index

urlpatterns = [
    url(r'^$', index, name='index'),
    url(r'^money/', include('money.urls')),
    url(r'^admin/', include(admin.site.urls)),
]

Создаём файл money/urls.py:

# -*- coding: utf-8 -*-
from django.conf.urls import url
from money.views import index, transactions, add_transaction, update

urlpatterns = [
 url(r'^$', index, name='index'),
 url(r'^index', index, name='index'),
 url(r'^transactions', transactions, name='transactions'),
 url(r'^add-transaction', add_transaction, name='add_transaction'),
 url(r'^update', update, name='update'),
]

Отдельный URL index нужен для переадресации на главную после добавления транзакции (с помощью django.core.urlresolvers.reverse()).

Создаём представления. В файл money/views.py добавляем:

# -*- coding: utf-8 -*-
from django.http import HttpResponse

def index(request):
    return HttpResponse('Index')

def transactions(request):
    return HttpResponse('transactions')

def add_transaction(request):
    return HttpResponse('transactions')

def update(request):
    return HttpResponse('update')

Сохраняем, проверяем:

$ curl -u user:pass http://money.domain.org.ua/
Index
$ curl -u user:pass http://money.domain.org.ua/money/transactions
transactions

URLconf работает.

Возвращаемся к представлениям.

Я намеренно не использую generic views тут, т.к. при таком виде – более понятно (самому себе, в первую очередь) – что и как работает. Подрасту – сделаю с общими представлениями 🙂

Приводим money/views.py к такому виду:

# -*- coding: utf-8 -*-
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.db.models import F, Sum

from models import Transactions, Transactions_types, Source_types

"""Главная страница со ссылками. суммами по каждому Source_types
   и целиком"""
def index(request):
    sources = Source_types.objects.all()

    names = Source_types.objects.values('type')
    sums = Source_types.objects.values_list('source_sum')
    total = Source_types.objects.aggregate(Sum('source_sum'))

    return render(request, 'index.html', {
            'sources': sources,
            'sums': sums,
            'names': names,
            'total': total.get('source_sum__sum'),
        })


"""Список всех последних транзакций"""
def transactions(request):
    latest_transaction_list = Transactions.objects.order_by('-transaction_date')
    types = Transactions_types.objects.all()
    sources = Source_types.objects.all()

    return render(request, 'transactions.html', {
            'latest_transaction_list': latest_transaction_list,
            'types': types,
            'sources': sources,
        })


"""Страница добавления транзакции"""
def add_transaction(request):
    types = Transactions_types.objects.all()
    sources = Source_types.objects.all()

    return render(request, 'add_transaction.html', {
            'types': types,
            'sources': sources,
        })

"""Представление, которое выполняет запись в та лица транзакций.
   После выполнения записи - переадресовывает на станицу списка тразакций"""
def update(request):
    if request.method == 'POST':
        date = request.POST['date']
        desc = request.POST['desc']
        sum = request.POST['sum']
        type = Transactions_types.objects.get(pk=(request.POST['type']))
        source = Source_types.objects.get(pk=(request.POST['source']))

        # обновляем запись в списке транзакций
        result = Transactions(transaction_type=type, source_type=source, transaction_date=date, transaction_sum=sum, transaction_desc=desc)
        result.save()

        # обновляем запись об остатке на source
        # не самое красивое решение - ID могут быть и не 1 для прихода, а 2 для расхода
        # а наоборот, но лучшего не придумал
        if type.id == 1:
            source.source_sum = F('source_sum') + sum
        elif type.id == 2:
            source.source_sum = F('source_sum') - sum

        source.save()

        return HttpResponseRedirect(reverse('index'))

Добавление шаблонов

Создаём каталог для шаблонов, находясь по прежнему в корневом каталоге проекта /var/www/django/money_domain_org_ua:

$ mkdir -p money/templates/

В нём создаём файлы шаблонов – base.html (будет родительским шаблоном), index.html, transactions.html, add_transaction.html, :

$ vim -p money/templates/index.html money/templates/transactions.html money/templates/add_transaction.html money/templates/base.html

Файл-шаблон base.html:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="ru">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <title>Домашняя бухгалтерия: {% block title %}{% endblock %}</title>
</head>
<body>
    {% block header %}
        <p><a href="{% url 'index' %}">На главную</a> | <a href="{% url 'transactions' %}">Транзакции</a> | <a href="{% url 'add_transaction' %}">Добавить транзакцию</a> |  <a href="http://money.setevoy.org.ua/admin">Админка</a></p>
    {% endblock %}
    <hr>
    {% block content %}
    {% endblock %}
    <hr>
    {% block footer %}
        <p><a href="{% url 'index' %}">На главную</a> | <a href="{% url 'transactions' %}">Транзакции</a> | <a href="{% url 'add_transaction' %}">Добавить транзакцию</a> |  <a href="http://money.setevoy.org.ua/admin">Админка</a></p>
    {% endblock %}
</body>
</html>

Содержимое index.html:

{% extends "base.html" %}

{% block title %}
    Главная
{% endblock %}

{% block content %}
    <table border="1">
        <tr><th>Тип источника</th><th>Остаток</th></tr>
        {% for source in sources %}
            <tr><td>{{ source.type }}</td><td>{{ source.source_sum }}</td></tr>
        {% endfor %}
    </table>

    <p>Всего: {{ total }}</p>
{% endblock %}

Файл transactions.html:

{% extends "base.html" %}

{% block title %}
    Список транзакций
{% endblock %}

{% block content %}

    {% if latest_transaction_list %}
        <table border="1">
            <tr><th>Описание</th><th>Дата</th><th>Сумма</th><th>Тип</th></tr>
            {% for transaction in latest_transaction_list %}
                {% if transaction %}
                    <tr><td>{{ transaction.transaction_desc }}</td><td>{{ transaction.transaction_date }}</td><td>{{ transaction.transaction_sum }}</td><td>{{ transaction.transaction_type }}</td></tr>
                {% endif %}
            {% endfor %}
        </table>
    {% else %}
        <p>No transaction are available.</p>
    {% endif %}
{% endblock %}

И шаблон add_transaction.html:

{% extends "base.html" %}

{% block title %}
    Добавление тразакции
{% endblock %}

{% block content %}

    <form action="{% url 'update' %}" method="post">
        <fieldset>
            {% csrf_token %}

            <p>Выбери расход или приход:</p>
            <p> {% for type in types %}
                    <input type="radio" name="type" id="type{{ forloop.counter }}" value="{{ type.id }}"/>
                    <label for="type{{ forloop.counter }}">{{ type.type }}</label><br />
                {% endfor %}</p>

            <p>Выбери тип платежа:</p>
            <p> {% for source in sources %}
                    <input type="radio" name="source" id="source{{ forloop.counter }}" value="{{ source.id }}"/>
                    <label for="type{{ forloop.counter }}">{{ source.type }}</label><br />
                {% endfor %}</p>


            <p><label for="date">Дата: </label>
               <input type="date" name="date"></p>

            <p><label for="sum">Сумма: </label>
               <input type="text" name="sum" style="width: 70px;"/></p>

               <textarea rows="4" cols="50" name="desc" placeholder="Описание"></textarea>

               <input type="submit" value="Добавить" />
        </fieldset>
    </form>
{% endblock %}

Редактируем файл setevoy_money/settings.py и добавляем DIRS в TEMPLATES:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [[os.path.join(BASE_DIR, 'templates')]],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Перезапускаем сервер, проверяем (тут я уже добавил второй source):

money_7

Главная страница

Список совершённых транзакций

Список совершённых транзакций

Добавление тразакции

Добавление тразакции

Вот, как-то так оно всё работает.

Для создания использовались данные, полученные из циклов переводов:
Django Book – русский перевод
Django: пример создания приложения