Browse Source

big refactoring, most notably modularization

- splitted 'cashonly' into two separate apps named 'cashonly.web' and
   'cashonly.core'
 - added a service layer as a place for business logic related to the Account
   model
 - adapted admin and web functionality to use the new AccountManager from the
   service layer
 - added a debit_limit field to the Account model in order to get rid of the
   formerly hard-coded default
 - probably other changes that I've forgotten to mention

Ther are likely some bugs and TODOs still left behind
master
Fr3deric 7 years ago
parent
commit
6feb3df353
  1. 22
      cashonly/auth.py
  2. 12
      cashonly/core/__init__.py
  3. 34
      cashonly/core/admin.py
  4. 6
      cashonly/core/apps.py
  5. 0
      cashonly/core/formfields.py
  6. 0
      cashonly/core/management/__init__.py
  7. 0
      cashonly/core/management/commands/__init__.py
  8. 44
      cashonly/core/management/commands/dailydigest.py
  9. 0
      cashonly/core/management/commands/debtreminder.py
  10. 109
      cashonly/core/migrations/0001_initial.py
  11. 20
      cashonly/core/migrations/0002_account_debit_limit.py
  12. 0
      cashonly/core/migrations/__init__.py
  13. 191
      cashonly/core/models.py
  14. 114
      cashonly/core/services.py
  15. 0
      cashonly/core/templates/cashonly/core/daily_digest.txt
  16. 442
      cashonly/locale/de/LC_MESSAGES/django.po
  17. 201
      cashonly/models.py
  18. 1
      cashonly/templates/cashonly/index.html
  19. 16
      cashonly/tests.py
  20. 1
      cashonly/version.py
  21. 9
      cashonly/web/__init__.py
  22. 0
      cashonly/web/static/images/no-image.png
  23. 0
      cashonly/web/static/signin.css
  24. 0
      cashonly/web/static/style.css
  25. 4
      cashonly/web/templates/cashonly/web/base.html
  26. 2
      cashonly/web/templates/cashonly/web/buy_confirm.html
  27. 2
      cashonly/web/templates/cashonly/web/buy_error.html
  28. 2
      cashonly/web/templates/cashonly/web/buy_thanks.html
  29. 0
      cashonly/web/templates/cashonly/web/debt_reminder.txt
  30. 0
      cashonly/web/templates/cashonly/web/includes/product_list.html
  31. 0
      cashonly/web/templates/cashonly/web/includes/transaction_list.html
  32. 1
      cashonly/web/templates/cashonly/web/index.html
  33. 0
      cashonly/web/templates/cashonly/web/login.html
  34. 2
      cashonly/web/templates/cashonly/web/product_detail.html
  35. 4
      cashonly/web/templates/cashonly/web/product_list.html
  36. 4
      cashonly/web/templates/cashonly/web/transaction_list.html
  37. 2
      cashonly/web/templates/cashonly/web/usersettings.html
  38. 2
      cashonly/web/templates/cashonly/web/usersettings_saved.html
  39. 2
      cashonly/web/urls.py
  40. 37
      cashonly/web/views.py

22
cashonly/auth.py

@ -1,22 +0,0 @@ @@ -1,22 +0,0 @@
from django.contrib.auth.models import User
from cashonly.models import Account
class CashBackend(object):
def authenticate(self, card_number=None, pin=None):
if not card_number.isdigit():
raise ValueError('card_number has to consist of digits only!')
if len(pin) > 0 and not pin.isdigit():
raise ValueError('pin has to consist of digits only or \
has to be empty!')
try:
account = Account.objects.get(card_number=card_number)
except Account.DoesNotExist:
return None
if account.check_pin(pin):
return account.user
return None

12
cashonly/core/__init__.py

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
from django.apps import AppConfig
class CashonlyCoreAppConfig(AppConfig):
name = 'cashonly.core'
label = 'cashonly_core'
def ready(self):
from . import services
default_app_config = 'cashonly.core.CashonlyCoreAppConfig'

34
cashonly/admin.py → cashonly/core/admin.py

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
from django.contrib import admin
from cashonly.models import *
from cashonly.formfields import DigitField
from cashonly.core.models import *
from cashonly.core.formfields import DigitField
from cashonly.core.services import AccountManager
from django import forms
from django.template.defaultfilters import escape
from django.core.urlresolvers import reverse
@ -53,7 +54,7 @@ class AccountAdmin(admin.ModelAdmin): @@ -53,7 +54,7 @@ class AccountAdmin(admin.ModelAdmin):
def transaction_link(self, account):
return '<a href="%s?account__id__exact=%d">%s</a>' % \
(reverse("admin:cashonly_transaction_changelist"), account.id,
(reverse("admin:cashonly_core_transaction_changelist"), account.id,
_('Transactions'))
transaction_link.short_description = ugettext_lazy('Transaction link')
@ -61,34 +62,21 @@ class AccountAdmin(admin.ModelAdmin): @@ -61,34 +62,21 @@ class AccountAdmin(admin.ModelAdmin):
transaction_link.allow_tags = True
def save_model(self, request, obj, form, change):
accmgr = AccountManager(obj)
pin = form.cleaned_data['pin_change']
pin_empty = form.cleaned_data['pin_empty']
if pin_empty:
obj.clear_pin()
else:
if pin is not None and len(pin) != 0:
obj.set_pin(pin)
PAYOUT_SUBJECT = ugettext_noop('Payout')
DEPOSIT_SUBJECT = ugettext_noop('Deposit')
DESCRIPTION = ugettext_noop('Authorized by %(first)s %(last)s')
accmgr.clear_pin()
elif pin is not None and len(pin) != 0:
accmgr.set_pin(pin)
amount = form.cleaned_data['credit_change']
comment = form.cleaned_data['credit_change_comment']
if amount is not None and amount != 0:
if amount > 0:
subject = DEPOSIT_SUBJECT
else:
subject = PAYOUT_SUBJECT
desc = DESCRIPTION % {'first': request.user.first_name,
'last': request.user.last_name}
if comment is not None and len(comment) > 0:
desc += ' (%s)' % (comment)
obj.change_credit(amount, subject, desc)
accmgr.change_credit(amount, request.user, comment)
# TODO: check whether this can be dropped
# Make sure the object is saved in any case
obj.save()

6
cashonly/core/apps.py

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
from django.apps import AppConfig
class CashonlyCoreConfig(AppConfig):
name = 'cashonly.core'
label = 'cashonly_core'

0
cashonly/formfields.py → cashonly/core/formfields.py

0
cashonly/__init__.py → cashonly/core/management/__init__.py

0
cashonly/management/__init__.py → cashonly/core/management/commands/__init__.py

44
cashonly/management/commands/dailydigest.py → cashonly/core/management/commands/dailydigest.py

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
from cashonly.models import *
from cashonly.core.models import *
from django.conf import settings
from django.core.mail import send_mass_mail
from django.core.management.base import NoArgsCommand, CommandError
from django.core.management.base import BaseCommand, CommandError
from django.template import Context
from django.template.loader import get_template
from django.utils import translation
@ -10,30 +10,30 @@ from django.utils.formats import get_format @@ -10,30 +10,30 @@ from django.utils.formats import get_format
from django.utils.translation import ugettext as _
import datetime
RANGE = 24
USERSETTINGS_URL = 'https://cypher/kasse/usersettings/'
class Command(BaseCommand):
help = 'Sends out a digest of transactions to recently active users'
class Command(NoArgsCommand):
help = 'Sends out the daily digest to all users with transactions' + \
'in the last %dh' % RANGE
def handle_noargs(self, **options):
def handle(self, **options):
translation.activate('de')
tpl = get_template('cashonly/daily_digest.txt')
tpl = get_template('cashonly/core/daily_digest.txt')
messages = []
for a in Account.objects.all():
for a in Account.objects.filter(daily_digest=True):
name = '%s %s' % (a.user.first_name, a.user.last_name)
context = {'name': name,
'credit': a.credit,
'range': RANGE,
'url': USERSETTINGS_URL}
context = {
'name': name,
'credit': a.credit,
'range': settings.CASHONLY_DAILY_DIGEST_RANGE_HOURS,
'url': settings.CASHONLY_USERSETTINGS_URL,
}
min_ts = datetime.datetime.now() - datetime.timedelta(
hours=settings.CASHONLY_DAILY_DIGEST_RANGE_HOURS
)
transactions = Transaction.objects.filter(account=a) \
.filter(timestamp__gte=(datetime.datetime.now() -
datetime.timedelta(hours=RANGE)))
.filter(timestamp__gte=min_ts)
if transactions.count() > 0:
lengths = {'timestamp': len(_('date')),
@ -59,11 +59,17 @@ class Command(NoArgsCommand): @@ -59,11 +59,17 @@ class Command(NoArgsCommand):
context['tl'] = transactions
context['sum'] = sum
rcpts = ['%s <%s>' % (name, a.user.email)]
if a.user.email is not None and len(a.user.email) > 0:
rcpts = ['%s <%s>' % (name, a.user.email)]
else:
self.stdout.write(self.style.WARNING(
'User "%s" has no Email address.' % (a.user.username)
))
continue
messages.append(('%s%s' % (settings.EMAIL_SUBJECT_PREFIX,
_('Account Statement')),
tpl.render(Context(context)),
tpl.render(context),
settings.DEFAULT_FROM_EMAIL, rcpts))
send_mass_mail(tuple(messages))

0
cashonly/management/commands/debtreminder.py → cashonly/core/management/commands/debtreminder.py

109
cashonly/core/migrations/0001_initial.py

@ -0,0 +1,109 @@ @@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-30 21:40
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Account',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('card_number', models.CharField(blank=True, max_length=32, null=True, unique=True, verbose_name='card number')),
('pin', models.CharField(blank=True, max_length=32, verbose_name='PIN')),
('daily_digest', models.BooleanField(default=True, verbose_name='daily digest')),
('credit', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='credit')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'account',
'verbose_name_plural': 'accounts',
},
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=32, unique=True, verbose_name='name')),
('price', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='price')),
('active', models.BooleanField(default=True, verbose_name='active')),
('image', models.ImageField(blank=True, null=True, upload_to='products', verbose_name='image')),
('image_thumbnail', models.ImageField(blank=True, null=True, upload_to='products_thumb', verbose_name='image')),
],
options={
'verbose_name': 'product',
'verbose_name_plural': 'products',
},
),
migrations.CreateModel(
name='ProductBarcode',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('barcode', models.CharField(max_length=32, unique=True, verbose_name='barcode')),
('comment', models.CharField(blank=True, max_length=128, verbose_name='comment')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cashonly_core.Product', verbose_name='product')),
],
options={
'verbose_name': 'barcode',
'verbose_name_plural': 'barcodes',
},
),
migrations.CreateModel(
name='ProductCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=32, unique=True, verbose_name='name')),
('comment', models.CharField(blank=True, max_length=128, verbose_name='comment')),
],
options={
'verbose_name': 'product category',
'verbose_name_plural': 'product categories',
},
),
migrations.CreateModel(
name='SalesLogEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('count', models.IntegerField(verbose_name='count')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='unit price')),
('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='timestamp')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cashonly_core.Account', verbose_name='account')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cashonly_core.Product', verbose_name='product')),
],
options={
'verbose_name': 'sales log entry',
'verbose_name_plural': 'sales log entries',
},
),
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='timestamp')),
('subject', models.CharField(max_length=32, verbose_name='subject')),
('description', models.TextField(verbose_name='description')),
('amount', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='amount')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cashonly_core.Account', verbose_name='account')),
],
options={
'verbose_name': 'transaction',
'verbose_name_plural': 'transactions',
},
),
migrations.AddField(
model_name='product',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cashonly_core.ProductCategory', verbose_name='category'),
),
]

20
cashonly/core/migrations/0002_account_debit_limit.py

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-30 22:12
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cashonly_core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='account',
name='debit_limit',
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Debit Limit'),
),
]

0
cashonly/management/commands/__init__.py → cashonly/core/migrations/__init__.py

191
cashonly/core/models.py

@ -0,0 +1,191 @@ @@ -0,0 +1,191 @@
from django.conf import settings
from django.db import models
from django.core.files import File
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
class Account(models.Model):
user = models.OneToOneField(
User
)
card_number = models.CharField(
verbose_name=_('card number'),
max_length=32,
unique=True,
blank=True,
null=True,
)
pin = models.CharField(
verbose_name=_('PIN'),
max_length=32,
blank=True,
)
daily_digest = models.BooleanField(
verbose_name=_('daily digest'),
default=True,
)
credit = models.DecimalField(
verbose_name=_('credit'),
max_digits=5,
decimal_places=2,
default=0,
)
debit_limit = models.DecimalField(
verbose_name=_('debit limit'),
max_digits=5,
decimal_places=2,
default=settings.CASHONLY_DEFAULT_DEBIT_LIMIT,
)
def __str__(self):
return self.user.username
class Meta:
verbose_name = _('account')
verbose_name_plural = _('accounts')
class ProductCategory(models.Model):
name = models.CharField(
verbose_name=_('name'),
max_length=32,
unique=True,
)
comment = models.CharField(
verbose_name=_('comment'),
max_length=128,
blank=True,
)
def __str__(self):
return '%s (%s)' % (self.name, self.comment)
class Meta:
verbose_name = _('product category')
verbose_name_plural = _('product categories')
class Product(models.Model):
name = models.CharField(
verbose_name=_('name'),
max_length=32,
unique=True,
)
price = models.DecimalField(
verbose_name=_('price'),
max_digits=5,
decimal_places=2,
)
active = models.BooleanField(
verbose_name=_('active'),
default=True,
)
category = models.ForeignKey(
ProductCategory,
verbose_name=_('category'),
blank=True,
null=True,
)
image = models.ImageField(
verbose_name=_('image'),
upload_to='products',
blank=True,
null=True,
)
image_thumbnail = models.ImageField(
verbose_name=_('image'),
upload_to='products_thumb',
blank=True,
null=True,
)
def __unicode__(self):
return self.name
class Meta:
verbose_name = _('product')
verbose_name_plural = _('products')
class ProductBarcode(models.Model):
barcode = models.CharField(
verbose_name=_('barcode'),
max_length=32,
unique=True,
)
comment = models.CharField(
verbose_name=_('comment'),
max_length=128,
blank=True,
)
product = models.ForeignKey(
Product,
verbose_name=_('product'),
)
def __unicode__(self):
return self.barcode
class Meta:
verbose_name = _('barcode')
verbose_name_plural = _('barcodes')
class Transaction(models.Model):
account = models.ForeignKey(
Account,
verbose_name=_('account'),
)
timestamp = models.DateTimeField(
verbose_name=_('timestamp'),
auto_now_add=True,
)
subject = models.CharField(
verbose_name=_('subject'),
max_length=32,
)
description = models.TextField(
verbose_name=_('description'),
)
amount = models.DecimalField(
verbose_name=_('amount'),
max_digits=5,
decimal_places=2,
)
class Meta:
verbose_name = _('transaction')
verbose_name_plural = _('transactions')
class SalesLogEntry(models.Model):
account = models.ForeignKey(
Account,
verbose_name=_('account'),
)
product = models.ForeignKey(
Product,
verbose_name=_('product'),
)
count = models.IntegerField(
verbose_name=_('count'),
)
unit_price = models.DecimalField(
verbose_name=_('unit price'),
max_digits=5,
decimal_places=2,
)
timestamp = models.DateTimeField(
verbose_name=_('timestamp'),
auto_now_add=True,
)
def __unicode__(self):
return '%dx %s - %s' % (self.count, self.product, self.account)
class Meta:
verbose_name = _('sales log entry')
verbose_name_plural = _('sales log entries')

114
cashonly/core/services.py

@ -0,0 +1,114 @@ @@ -0,0 +1,114 @@
from cashonly.core.models import Account, Transaction, SalesLogEntry, Product
from django.core.files import File
from django.contrib.auth.models import User
from django.utils.translation import ugettext_noop
from django.db.models.signals import pre_save, post_save, pre_delete
from django.dispatch import receiver
from django.db import transaction
import PIL.Image
import io
class AccountManager:
def __init__(self, account):
self.account = account
@transaction.atomic
def add_transaction(self, amount, subject, description):
self.account.refresh_from_db()
self.account.credit = self.account.credit + amount
self.account.save()
Transaction.objects.create(account=self.account, subject=subject,
amount=amount, description=description)
def change_credit(self, amount, operator, comment=None):
if amount > 0:
subject = ugettext_noop('Deposit')
elif amount < 0:
subject = ugettext_noop('Payout')
else:
raise ValueError('Amount must not be zero.')
desc = ugettext_noop('Authorized by %(first)s %(last)s') % \
{'first': operator.first_name, 'last': operator.last_name}
if comment is not None and len(comment) > 0:
desc += ' (%s)' % (comment)
self.add_transaction(amount, subject, desc)
@transaction.atomic
def buy_products(self, products):
if min(products.values()) <= 0:
raise ValueError('Non-positive amount in products dict.')
total_value = sum(map(lambda p: p.price * products[p],
products.keys()))
if self.account.credit - total_value >= self.account.debit_limit * -1:
desc = ''
for product in products.keys():
if not product.active:
raise ValueError('Trying to buy a disabled product.')
amount = products[product]
desc += '%d x %s\n' % (amount, product.name)
SalesLogEntry.objects.create(
account=self.account,
product=product,
count=amount,
unit_price=product.price
)
self.add_transaction(-total_value, ugettext_noop('Purchase'), desc)
return True
else:
return False
def buy_product(self, product, amount):
return self.buy_products({product: amount})
def set_pin(self, pin):
self.account.pin = pin
self.account.save()
def clear_pin(self):
self.set_pin('')
def check_pin(self, pin):
return self.account.pin == pin
@receiver(post_save, sender=User)
def user_post_save_handler(sender, instance, created, **kwargs):
# TODO: add possibility to disable this via settings variable
if created:
Account.objects.create(user=instance)
@receiver(pre_delete, sender=SalesLogEntry)
def logentry_pre_delete_handler(sender, instance, **kwargs):
accmgr = AccountManager(instance.account)
accmgr.add_transaction(
instance.unit_price * instance.count,
ugettext_noop('Cancellation'),
'%d x %s' % (instance.count, instance.product.name)
)
@receiver(pre_save, sender=Product)
def product_post_save_handler(sender, instance, **kwargs):
# FIXME
img = instance.image
if img:
scaledFile = io.StringIO()
img.open(mode='r')
with img:
scaled = PIL.Image.open(img)
thumbnail_size = getattr(settings, 'THUMBNAIL_SIZE', (150, 150))
scaled.thumbnail(thumbnail_size, PIL.Image.ANTIALIAS)
scaled.save(scaledFile, 'PNG')
scaledFile.seek(0)
instance.image_thumbnail.save(img.url, File(scaledFile), save=False)

0
cashonly/templates/cashonly/daily_digest.txt → cashonly/core/templates/cashonly/core/daily_digest.txt

442
cashonly/locale/de/LC_MESSAGES/django.po

@ -1,442 +0,0 @@ @@ -1,442 +0,0 @@
# German language file for django cashonly app.
# Copyright (C) 2013 Niklas Brachmann
# This file is distributed under the same license as the cashonly package.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-02-08 01:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
#: admin.py:14 models.py:170 management/commands/dailydigest.py:40
msgid "amount"
msgstr "Betrag"
#: admin.py:17 models.py:103 models.py:153
msgid "comment"
msgstr "Kommentar"
#: admin.py:22 models.py:19 views.py:112
msgid "PIN"
msgstr "PIN"
#: admin.py:25
msgid "clear PIN"
msgstr "PIN löschen"
#: admin.py:38
msgid "credit change"
msgstr "Ein-/Auszahlung"
#: admin.py:41
msgid "change PIN"
msgstr "PIN ändern"
#: admin.py:49
msgid "Transactions"
msgstr "Transaktionen"
#: admin.py:51
msgid "Transaction link"
msgstr "Transaktionslink"
#: admin.py:65 api.py:225
msgid "Payout"
msgstr "Auszahlung"
#: admin.py:66 api.py:226
msgid "Deposit"
msgstr "Einzahlung"
#: admin.py:67 api.py:227
#, python-format
msgid "Authorized by %(first)s %(last)s"
msgstr "Autorisiert von %(first)s %(last)s"
#: formfields.py:11
msgid "Please enter only digits."
msgstr "Bitte nur Ziffern eingeben."
#: models.py:18
msgid "card number"
msgstr "Kartennummer"
#: models.py:20 views.py:108
msgid "daily digest"
msgstr "Tägliche Zusammenfassung"
#: models.py:22
msgid "credit"
msgstr "Guthaben"
#: models.py:28 models.py:164 models.py:177
msgid "account"
msgstr "Konto"
#: models.py:29
msgid "accounts"
msgstr "Konten"
#: models.py:60
msgid "Purchase"
msgstr "Einkauf"
#: models.py:101 models.py:114
msgid "name"
msgstr "Name"
#: models.py:109
msgid "product category"
msgstr "Produktkategorie"
#: models.py:110
msgid "product categories"
msgstr "Produktkategorien"
#: models.py:116
msgid "price"
msgstr "Preis"
#: models.py:117
msgid "active"
msgstr "Aktiv"
#: models.py:119
msgid "category"
msgstr "Kategorie"
#: models.py:120 models.py:123
msgid "image"
msgstr ""
#: models.py:130 models.py:154 models.py:178
msgid "product"
msgstr "Produkt"
#: models.py:131
msgid "products"
msgstr "Produkte"
#: models.py:151 models.py:160
msgid "barcode"
msgstr "Strichcode"
#: models.py:161
msgid "barcodes"
msgstr "Strichcodes"
#: models.py:166 models.py:183
msgid "timestamp"
msgstr "Zeitstempel"
#: models.py:167 management/commands/dailydigest.py:39
msgid "subject"
msgstr "Betreff"
#: models.py:168
msgid "description"
msgstr "Beschreibung"
#: models.py:173
msgid "transaction"
msgstr "Transaktion"
#: models.py:174
msgid "transactions"
msgstr "Transaktionen"
#: models.py:179
msgid "count"
msgstr "Anzahl"
#: models.py:181
msgid "unit price"
msgstr "Stückpreis"
#: models.py:188
msgid "sales log entry"
msgstr "Verkaufsprotokolleintrag"
#: models.py:189
msgid "sales log entries"
msgstr "Verkaufsprotokolleinträge"
#: models.py:193
msgid "Cancellation"
msgstr "Stornierung"
#: views.py:114
msgid "PIN (confirmation)"
msgstr "PIN (bestätigen)"
#: views.py:124
msgid "PINs do not match."
msgstr "PINs stimmen nicht überein."
#: management/commands/dailydigest.py:38
msgid "date"
msgstr "Datum"
#: management/commands/dailydigest.py:64
msgid "Account Statement"
msgstr "Kontoauszug"
#: management/commands/debtreminder.py:28
msgid "Debt Reminder"
msgstr "Schuldenerinnerung"
#: templates/cashonly/base.html:22 templates/cashonly/buy_confirm.html:17
#: templates/cashonly/product_detail.html:11
msgid "Name"
msgstr "Name"
#: templates/cashonly/base.html:24 templates/cashonly/login.html:25
msgid "Username"
msgstr "Benutzername"
#: templates/cashonly/base.html:25
msgid "Credit"
msgstr "Guthaben"
#: templates/cashonly/base.html:27
msgid "Warning!"
msgstr "Achtung!"
#: templates/cashonly/base.html:27
#, python-format
msgid "Only %(debtlimitamount)s&nbsp;&euro; until reaching the debt limit."
msgstr ""
"Nur noch %(debtlimitamount)s&nbsp;&euro; bis zum Erreichen des "
"Schuldenlimits."
#: templates/cashonly/base.html:63
msgid "Overview"
msgstr "Übersicht"
#: templates/cashonly/base.html:64 templates/cashonly/includes/product_list.html:12
msgid "Buy"
msgstr "Kaufen"
#: templates/cashonly/base.html:65
msgctxt "monthly staement"
msgid "Transaction list"
msgstr "Kontoauszug"
#: templates/cashonly/base.html:66 templates/cashonly/usersettings.html:29
#: templates/cashonly/usersettings_saved.html:6
msgid "Preferences"
msgstr "Einstellungen"
#: templates/cashonly/base.html:68
msgid "Administration"
msgstr "Administration"
#: templates/cashonly/base.html:80
msgid "Logout"
msgstr "Abmelden"
#: templates/cashonly/base.html:92
#, python-format
msgid "Welcome, %(firstname)s!"
msgstr "Herzlich willkommen, %(firstname)s!"
#: templates/cashonly/base.html:98
msgid "Account information"
msgstr "Kontoinformationen"
#: templates/cashonly/base.html:102
msgid "Username:"
msgstr "Benutzername:"
#: templates/cashonly/base.html:104
msgid "Full name:"
msgstr "Realname:"
#: templates/cashonly/base.html:106
msgid "E-Mail address:"
msgstr "E-Mail Adresse:"
#: templates/cashonly/base.html:108
msgid "Credit:"
msgstr "Guthaben:"
#: templates/cashonly/base.html:117
msgid "Last purchased products"
msgstr "Zuletzt gekaufte Produkte"
#: templates/cashonly/base.html:127
msgid "Your transactions in the last 12 hours"
msgstr "Transaktionen in den vergangenen 12 Stunden"
#: templates/cashonly/base.html:133
msgid "There where no transactions in your account in the last 12 hours."
msgstr "In den vergangenen 12 Stunden wurden keine Transaktionen durchgeführt."
#: templates/cashonly/buy_confirm.html:8 templates/cashonly/buy_error.html:8
#: templates/cashonly/buy_thanks.html:8
msgid "Buy product"
msgstr "Produkt kaufen"
#: templates/cashonly/buy_confirm.html:13
msgid "Confirm purchase."
msgstr "Einkauf bestätigen."
#: templates/cashonly/buy_confirm.html:18 templates/cashonly/product_detail.html:12
msgid "Price"
msgstr "Preis"
#: templates/cashonly/buy_confirm.html:19 templates/cashonly/product_detail.html:13
msgid "Category"
msgstr "Kategorie"
#: templates/cashonly/buy_confirm.html:22
msgid "Do you really want to buy this product?"
msgstr "Möchtest du dieses Produkt wirklich kaufen?"
#: templates/cashonly/buy_confirm.html:23 templates/cashonly/buy_confirm.html.py:24
msgid "Yes"
msgstr "Ja"
#: templates/cashonly/buy_confirm.html:25
msgid "No"
msgstr "Nein"
#: templates/cashonly/buy_error.html:11
msgid "You cannot buy this product, as the debt limit has been reached."
msgstr ""
"Du kannst dieses Produkt nicht kaufen, da das Schuldenlimit erreicht wurde."
#: templates/cashonly/buy_error.html:14 templates/cashonly/buy_thanks.html:14
#: templates/cashonly/usersettings_saved.html:11
msgid "Back"
msgstr "Zurück"
#: templates/cashonly/buy_thanks.html:11
msgid "Thanks for your purchase!"
msgstr "Danke für deinen Einkauf."
#: templates/cashonly/login.html:24
msgid "Please sign in"
msgstr "Bitte anmelden."
#: templates/cashonly/login.html:26
msgid "Password"
msgstr "Passwort"
#: templates/cashonly/login.html:27
msgid "Sign in"
msgstr "Anmelden"
#: templates/cashonly/product_detail.html:8
msgid "Product details"
msgstr "Produktdetails"
#: templates/cashonly/product_list.html:10
msgid "All categories"
msgstr "Alle Kategorien"
#: templates/cashonly/transaction_list.html:7
msgctxt "monthly statement"
msgid "Transaction list"
msgstr "Kontoauszug"
#: templates/cashonly/transaction_list.html:13
msgid "less detailed"
msgstr "weniger Details"
#: templates/cashonly/transaction_list.html:15
msgid "more detailed"
msgstr "mehr Details"
#: templates/cashonly/transaction_list.html:24
#, python-format
msgid "Page %(current)s of %(num)s"
msgstr "Seite %(current)s von %(num)s"
#: templates/cashonly/transaction_list.html:30
msgid "No transactions have been made, yet."
msgstr "Es wurden noch keine Transaktionen durchgeführt."
#: templates/cashonly/usersettings.html:34
msgid "Daily digest"
msgstr "Tägliche Zusammenfassung"
#: templates/cashonly/usersettings.html:36
msgid ""
"The digest will be sent nightly, as long as there were transaction made in "
"the past 24 hours."
msgstr ""
"Der Kontoauszug wird nachts versandt, sofern in den vergangenen 24 Stunden "
"Kontobewegungen stattgefunden haben."
#: templates/cashonly/usersettings.html:40 templates/cashonly/usersettings.html:60
msgid "Save"
msgstr ""
#: templates/cashonly/usersettings.html:52
msgid "Change PIN"
msgstr "PIN ändern"
#: templates/cashonly/usersettings.html:54
msgid ""
"The PIN is asked for after scanning the member's ID card. If this field is "
"left blank, no PIN will be needed to log in."
msgstr ""
"Der PIN wird nach dem Einscannen des Mitgliederausweises abgefragt. Wenn das "
"Feld leergelassen wird, wird kein PIN abgefragt."
#: templates/cashonly/usersettings.html:75 templates/cashonly/usersettings.html:82
msgid "Clear PIN"
msgstr "PIN löschen"
#: templates/cashonly/usersettings.html:78
msgid "Do you really want to clear your PIN?"
msgstr "Möchtest du deinen PIN wirklich löschen?"
#: templates/cashonly/usersettings.html:81
msgid "Cancel"
msgstr "Abbrechen"
#: templates/cashonly/usersettings_saved.html:9
msgid "The settings have been saved successfully!"
msgstr "Die Einstellungen wurden erfolgreich gespeichert."
#: templates/cashonly/includes/product_list.html:8
msgid "Product image"
msgstr "Produktbild"
#: templates/cashonly/includes/transaction_list.html:7
msgid "Date"
msgstr "Datum"
#: templates/cashonly/includes/transaction_list.html:8
msgid "Subject"
msgstr "Betreff"
#: templates/cashonly/includes/transaction_list.html:9
msgid "Description"
msgstr "Beschreibung"
#: templates/cashonly/includes/transaction_list.html:10
msgctxt "money"
msgid "amount"
msgstr "Betrag"
#~ msgid "Actions"
#~ msgstr "Aktionen"
#~ msgid "Details"
#~ msgstr "Details"
#~ msgctxt "login"
#~ msgid "Remember me"
#~ msgstr "Angemeldet bleiben"

201
cashonly/models.py

@ -1,201 +0,0 @@ @@ -1,201 +0,0 @@
from django.conf import settings
from django.db import models
from django.core.files import File
from django.contrib.auth.models import User
from django.db.models.signals import pre_save, post_save
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_noop
from django.db import transaction
import PIL.Image
import io
class Account(models.Model):
user = models.OneToOneField(User)
card_number = models.CharField(max_length=32, unique=True, blank=True,
null=True, verbose_name=_('card number'))
pin = models.CharField(max_length=32, blank=True, verbose_name=_('PIN'))
daily_digest = models.BooleanField(verbose_name=_('daily digest'),
default=True)
credit = models.DecimalField(max_digits=5, decimal_places=2, default=0,
verbose_name=_('credit'))
def __unicode__(self):
return self.user.username
class Meta:
verbose_name = _('account')
verbose_name_plural = _('accounts')
@receiver(post_save, sender=User)
def user_post_save_handler(sender, instance, created, **kwargs):
if created:
account = Account(user=instance)
account.save()
@transaction.atomic
def change_credit(self, amount, subject, desc):
# For atomicity fetch current value first
cur = Account.objects.filter(pk=self.pk)[0]
self.credit = cur.credit + amount
self.save()
trans = Transaction(account=self, subject=subject,
amount=amount, description=desc)
trans.save()
def buy_products(self, products):
# TODO place it somewhere else
MAX_DEBIT = -35
BUY_SUBJECT = ugettext_noop('Purchase')
if min(products.values()) <= 0:
raise ValueError('Non-positive amount in products dict.')
total_value = sum(map(lambda p: p.price * products[p],
products.keys()))
if self.credit - total_value >= MAX_DEBIT:
desc = ''
for product in products.keys():
if not product.active:
raise ValueError('Trying to buy a disabled product.')
amount = products[product]
logentry = SalesLogEntry(account=self, product=product,
count=amount,
unit_price=product.price)
logentry.save()
desc += '%d x %s\n' % (amount, product.name)
self.change_credit(-total_value, BUY_SUBJECT, desc)
return True
else:
return False
def buy_product(self, product, amount=1):
return self.buy_products({product: amount})
def set_pin(self, pin):
# TODO: hash pin
self.pin = pin
self.save()
def clear_pin(self):
self.pin = ''
self.save()
def check_pin(self, pin):
return pin == self.pin
class ProductCategory(models.Model):
name = models.CharField(max_length=32, unique=True,
verbose_name=_('name'))
comment = models.CharField(max_length=128, blank=True,
verbose_name=_('comment'))
def __unicode__(self):
return "%s (%s)" % (self.name, self.comment)
class Meta:
verbose_name = _('product category')
verbose_name_plural = _('product categories')
class Product(models.Model):
name = models.CharField(max_length=32, unique=True,
verbose_name=_('name'))
price = models.DecimalField(max_digits=5, decimal_places=2,
verbose_name=_('price'))
active = models.BooleanField(default=True, verbose_name=_('active'))
category = models.ForeignKey(ProductCategory, blank=True, null=True,
verbose_name=_('category'))
image = models.ImageField(upload_to="products", verbose_name=_('image'),
blank=True, null=True)
image_thumbnail = models.ImageField(upload_to="products_thumb",
verbose_name=_('image'),
blank=True, null=True)
def __unicode__(self):
return self.name
class Meta:
verbose_name = _('product')
verbose_name_plural = _('products')
@receiver(pre_save, sender=Product)
def product_post_save_handler(sender, instance, **kwargs):
# FIXME
img = instance.image
if img:
scaledFile = io.StringIO()
img.open(mode='r')
with img:
scaled = PIL.Image.open(img)
thumbnail_size = getattr(settings, 'THUMBNAIL_SIZE', (150, 150))
scaled.thumbnail(thumbnail_size, PIL.Image.ANTIALIAS)
scaled.save(scaledFile, 'PNG')
scaledFile.seek(0)
instance.image_thumbnail.save(img.url, File(scaledFile), save=False)
class ProductBarcode(models.Model):
barcode = models.CharField(max_length=32, unique=True,
verbose_name=_('barcode'))
comment = models.CharField(max_length=128, blank=True,
verbose_name=_('comment'))
product = models.ForeignKey(Product, verbose_name=_('product'))
def __unicode__(self):
return self.barcode
class Meta:
verbose_name = _('barcode')
verbose_name_plural = _('barcodes')
class Transaction(models.Model):
account = models.ForeignKey(Account, verbose_name=_('account'))
timestamp = models.DateTimeField(auto_now_add=True,
verbose_name=_('timestamp'))
subject = models.CharField(max_length=32, verbose_name=_('subject'))
description = models.TextField(verbose_name=_('description'))
amount = models.DecimalField(max_digits=5, decimal_places=2,
verbose_name=_('amount'))
class Meta:
verbose_name = _('transaction')
verbose_name_plural = _('transactions')
class SalesLogEntry(models.Model):
account = models.ForeignKey(Account, verbose_name=_('account'))
product = models.ForeignKey(Product, verbose_name=_('product'))
count = models.IntegerField(verbose_name=_('count'))
unit_price = models.DecimalField(max_digits=5, decimal_places=2,
verbose_name=_('unit price'))
timestamp = models.DateTimeField(auto_now_add=True,
verbose_name=_('timestamp'))
def __unicode__(self):
return '%dx %s - %s' % (self.count, self.product, self.account)
class Meta:
verbose_name = _('sales log entry')
verbose_name_plural = _('sales log entries')
@receiver(pre_delete, sender=SalesLogEntry)
def logentry_pre_delete_handler(sender, instance, **kwargs):
SUBJECT = ugettext_noop('Cancellation')
DESC = '%d x %s'
instance.account.change_credit(
instance.unit_price * instance.count,
SUBJECT, DESC % (instance.count, instance.product.name)
)

1
cashonly/templates/cashonly/index.html

@ -1 +0,0 @@ @@ -1 +0,0 @@
{% extends "cashonly/base.html" %}

16
cashonly/tests.py

@ -1,16 +0,0 @@ @@ -1,16 +0,0 @@
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)

1
cashonly/version.py

@ -1 +0,0 @@ @@ -1 +0,0 @@
CASHONLY_VERSION = '2.1.2'

9
cashonly/web/__init__.py

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
from django.apps import AppConfig
class CashonlyWebAppConfig(AppConfig):
name = 'cashonly.web'
label = 'cashonly_web'
default_app_config = 'cashonly.web.CashonlyWebAppConfig'

0
cashonly/static/images/no-image.png → cashonly/web/static/images/no-image.png

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

0
cashonly/static/signin.css → cashonly/web/static/signin.css

0
cashonly/static/style.css → cashonly/web/static/style.css

4
cashonly/templates/cashonly/base.html → cashonly/web/templates/cashonly/web/base.html

@ -116,7 +116,7 @@ @@ -116,7 +116,7 @@
{% trans "Last purchased products" %}
</div>
<div class="panel-body">
{% include "cashonly/includes/product_list.html" with products=latest_purchases class="col-xs-6 col-md-4" only %}
{% include "cashonly/web/includes/product_list.html" with products=latest_purchases class="col-xs-6 col-md-4" only %}
</div>
</div>
</div>
@ -126,7 +126,7 @@ @@ -126,7 +126,7 @@
{% trans "Your transactions in the last 12 hours" %}
</div>
{% if latest_transactions %}
{% include "cashonly/includes/transaction_list.html" with transactions=latest_transactions only %}
{% include "cashonly/web/includes/transaction_list.html" with transactions=latest_transactions only %}
{% else %}
<div class="panel-body">
{% blocktrans %}There where no transactions in your account in the last 12 hours.{% endblocktrans %}

2
cashonly/templates/cashonly/buy_confirm.html → cashonly/web/templates/cashonly/web/buy_confirm.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends "cashonly/base.html" %}
{% extends "cashonly/web/base.html" %}
{% load i18n %}

2
cashonly/templates/cashonly/buy_error.html → cashonly/web/templates/cashonly/web/buy_error.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends "cashonly/base.html" %}
{% extends "cashonly/web/base.html" %}
{% load i18n %}

2
cashonly/templates/cashonly/buy_thanks.html → cashonly/web/templates/cashonly/web/buy_thanks.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends "cashonly/base.html" %}
{% extends "cashonly/web/base.html" %}
{% load i18n %}

0
cashonly/templates/cashonly/debt_reminder.txt → cashonly/web/templates/cashonly/web/debt_reminder.txt

0
cashonly/templates/cashonly/includes/product_list.html → cashonly/web/templates/cashonly/web/includes/product_list.html

0
cashonly/templates/cashonly/includes/transaction_list.html → cashonly/web/templates/cashonly/web/includes/transaction_list.html

1
cashonly/web/templates/cashonly/web/index.html

@ -0,0 +1 @@ @@ -0,0 +1 @@
{% extends "cashonly/web/base.html" %}

0
cashonly/templates/cashonly/login.html → cashonly/web/templates/cashonly/web/login.html

2
cashonly/templates/cashonly/product_detail.html → cashonly/web/templates/cashonly/web/product_detail.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends "cashonly/base.html" %}
{% extends "cashonly/web/base.html" %}
{% load i18n %}

4
cashonly/templates/cashonly/product_list.html → cashonly/web/templates/cashonly/web/product_list.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends "cashonly/base.html" %}
{% extends "cashonly/web/base.html" %}
{% load static %}
{% load i18n %}
@ -15,6 +15,6 @@ @@ -15,6 +15,6 @@
<!-- Ugly workaround -->
<br/>
{% include "cashonly/includes/product_list.html" with products=product_list only %}
{% include "cashonly/web/includes/product_list.html" with products=product_list only %}
{% endblock %}

4
cashonly/templates/cashonly/transaction_list.html → cashonly/web/templates/cashonly/web/transaction_list.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends "cashonly/base.html" %}
{% extends "cashonly/web/base.html" %}
{% load i18n %}
@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
<br/>
</div>
{% include "cashonly/includes/transaction_list.html" with transactions=transaction_list only %}
{% include "cashonly/web/includes/transaction_list.html" with transactions=transaction_list only %}
<ul class="pagination">
<li {% if not transaction_list.has_previous %}class="disabled"{% endif %}><a href="{% if transaction_list.has_previous %}{% if detailed %}{% url 'transactions_detailed' page=transaction_list.previous_page_number %}{% else %}{% url 'transactions' page=transaction_list.previous_page_number %}{% endif %}{% endif %}">&laquo;</a></li>

2
cashonly/templates/cashonly/usersettings.html → cashonly/web/templates/cashonly/web/usersettings.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends "cashonly/base.html" %}
{% extends "cashonly/web/base.html" %}
{% load i18n %}
{% load bootstrap %}
{% load staticfiles %}

2
cashonly/templates/cashonly/usersettings_saved.html → cashonly/web/templates/cashonly/web/usersettings_saved.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends "cashonly/base.html" %}
{% extends "cashonly/web/base.html" %}
{% load i18n %}
{% block content %}

2
cashonly/urls.py → cashonly/web/urls.py

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
from django.conf.urls import url
from cashonly import views
from cashonly.web import views
urlpatterns = [
url(r'^$', views.overview, name='overview'),

37
cashonly/views.py → cashonly/web/views.py

@ -3,15 +3,16 @@ from django import forms @@ -3,15 +3,16 @@ from django import forms
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.core import paginator
from cashonly.models import *
from cashonly.core.models import *
from cashonly.core.services import AccountManager
from django.utils.translation import ugettext as _