diff --git a/cashonly/auth.py b/cashonly/auth.py deleted file mode 100644 index a0263ad..0000000 --- a/cashonly/auth.py +++ /dev/null @@ -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 diff --git a/cashonly/core/__init__.py b/cashonly/core/__init__.py new file mode 100644 index 0000000..eb23024 --- /dev/null +++ b/cashonly/core/__init__.py @@ -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' diff --git a/cashonly/admin.py b/cashonly/core/admin.py similarity index 83% rename from cashonly/admin.py rename to cashonly/core/admin.py index d63c961..6773aa0 100644 --- a/cashonly/admin.py +++ b/cashonly/core/admin.py @@ -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): def transaction_link(self, account): return '%s' % \ - (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): 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() diff --git a/cashonly/core/apps.py b/cashonly/core/apps.py new file mode 100644 index 0000000..1224743 --- /dev/null +++ b/cashonly/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CashonlyCoreConfig(AppConfig): + name = 'cashonly.core' + label = 'cashonly_core' diff --git a/cashonly/formfields.py b/cashonly/core/formfields.py similarity index 100% rename from cashonly/formfields.py rename to cashonly/core/formfields.py diff --git a/cashonly/__init__.py b/cashonly/core/management/__init__.py similarity index 100% rename from cashonly/__init__.py rename to cashonly/core/management/__init__.py diff --git a/cashonly/management/__init__.py b/cashonly/core/management/commands/__init__.py similarity index 100% rename from cashonly/management/__init__.py rename to cashonly/core/management/commands/__init__.py diff --git a/cashonly/management/commands/dailydigest.py b/cashonly/core/management/commands/dailydigest.py similarity index 61% rename from cashonly/management/commands/dailydigest.py rename to cashonly/core/management/commands/dailydigest.py index 4dcea6f..ddbfd97 100644 --- a/cashonly/management/commands/dailydigest.py +++ b/cashonly/core/management/commands/dailydigest.py @@ -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 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): 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)) diff --git a/cashonly/management/commands/debtreminder.py b/cashonly/core/management/commands/debtreminder.py similarity index 100% rename from cashonly/management/commands/debtreminder.py rename to cashonly/core/management/commands/debtreminder.py diff --git a/cashonly/core/migrations/0001_initial.py b/cashonly/core/migrations/0001_initial.py new file mode 100644 index 0000000..c6d19d2 --- /dev/null +++ b/cashonly/core/migrations/0001_initial.py @@ -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'), + ), + ] diff --git a/cashonly/core/migrations/0002_account_debit_limit.py b/cashonly/core/migrations/0002_account_debit_limit.py new file mode 100644 index 0000000..7ddd9e6 --- /dev/null +++ b/cashonly/core/migrations/0002_account_debit_limit.py @@ -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'), + ), + ] diff --git a/cashonly/management/commands/__init__.py b/cashonly/core/migrations/__init__.py similarity index 100% rename from cashonly/management/commands/__init__.py rename to cashonly/core/migrations/__init__.py diff --git a/cashonly/core/models.py b/cashonly/core/models.py new file mode 100644 index 0000000..bab75a3 --- /dev/null +++ b/cashonly/core/models.py @@ -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') + + diff --git a/cashonly/core/services.py b/cashonly/core/services.py new file mode 100644 index 0000000..caba6e4 --- /dev/null +++ b/cashonly/core/services.py @@ -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) + + diff --git a/cashonly/templates/cashonly/daily_digest.txt b/cashonly/core/templates/cashonly/core/daily_digest.txt similarity index 100% rename from cashonly/templates/cashonly/daily_digest.txt rename to cashonly/core/templates/cashonly/core/daily_digest.txt diff --git a/cashonly/locale/de/LC_MESSAGES/django.po b/cashonly/locale/de/LC_MESSAGES/django.po deleted file mode 100644 index 5001852..0000000 --- a/cashonly/locale/de/LC_MESSAGES/django.po +++ /dev/null @@ -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 \n" -"Language-Team: LANGUAGE \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 € until reaching the debt limit." -msgstr "" -"Nur noch %(debtlimitamount)s € 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" diff --git a/cashonly/models.py b/cashonly/models.py deleted file mode 100644 index 7fcb913..0000000 --- a/cashonly/models.py +++ /dev/null @@ -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) - ) diff --git a/cashonly/templates/cashonly/index.html b/cashonly/templates/cashonly/index.html deleted file mode 100644 index 3cbf89a..0000000 --- a/cashonly/templates/cashonly/index.html +++ /dev/null @@ -1 +0,0 @@ -{% extends "cashonly/base.html" %} diff --git a/cashonly/tests.py b/cashonly/tests.py deleted file mode 100644 index 501deb7..0000000 --- a/cashonly/tests.py +++ /dev/null @@ -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) diff --git a/cashonly/version.py b/cashonly/version.py deleted file mode 100644 index 97b7d39..0000000 --- a/cashonly/version.py +++ /dev/null @@ -1 +0,0 @@ -CASHONLY_VERSION = '2.1.2' diff --git a/cashonly/web/__init__.py b/cashonly/web/__init__.py new file mode 100644 index 0000000..8721777 --- /dev/null +++ b/cashonly/web/__init__.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class CashonlyWebAppConfig(AppConfig): + name = 'cashonly.web' + label = 'cashonly_web' + + +default_app_config = 'cashonly.web.CashonlyWebAppConfig' diff --git a/cashonly/static/images/no-image.png b/cashonly/web/static/images/no-image.png similarity index 100% rename from cashonly/static/images/no-image.png rename to cashonly/web/static/images/no-image.png diff --git a/cashonly/static/signin.css b/cashonly/web/static/signin.css similarity index 100% rename from cashonly/static/signin.css rename to cashonly/web/static/signin.css diff --git a/cashonly/static/style.css b/cashonly/web/static/style.css similarity index 100% rename from cashonly/static/style.css rename to cashonly/web/static/style.css diff --git a/cashonly/templates/cashonly/base.html b/cashonly/web/templates/cashonly/web/base.html similarity index 95% rename from cashonly/templates/cashonly/base.html rename to cashonly/web/templates/cashonly/web/base.html index d01c902..26265eb 100644 --- a/cashonly/templates/cashonly/base.html +++ b/cashonly/web/templates/cashonly/web/base.html @@ -116,7 +116,7 @@ {% trans "Last purchased products" %}
- {% 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 %}
@@ -126,7 +126,7 @@ {% trans "Your transactions in the last 12 hours" %} {% 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 %}
{% blocktrans %}There where no transactions in your account in the last 12 hours.{% endblocktrans %} diff --git a/cashonly/templates/cashonly/buy_confirm.html b/cashonly/web/templates/cashonly/web/buy_confirm.html similarity index 96% rename from cashonly/templates/cashonly/buy_confirm.html rename to cashonly/web/templates/cashonly/web/buy_confirm.html index 69619a3..6392129 100644 --- a/cashonly/templates/cashonly/buy_confirm.html +++ b/cashonly/web/templates/cashonly/web/buy_confirm.html @@ -1,4 +1,4 @@ -{% extends "cashonly/base.html" %} +{% extends "cashonly/web/base.html" %} {% load i18n %} diff --git a/cashonly/templates/cashonly/buy_error.html b/cashonly/web/templates/cashonly/web/buy_error.html similarity index 88% rename from cashonly/templates/cashonly/buy_error.html rename to cashonly/web/templates/cashonly/web/buy_error.html index 82a314c..6edfef1 100644 --- a/cashonly/templates/cashonly/buy_error.html +++ b/cashonly/web/templates/cashonly/web/buy_error.html @@ -1,4 +1,4 @@ -{% extends "cashonly/base.html" %} +{% extends "cashonly/web/base.html" %} {% load i18n %} diff --git a/cashonly/templates/cashonly/buy_thanks.html b/cashonly/web/templates/cashonly/web/buy_thanks.html similarity index 87% rename from cashonly/templates/cashonly/buy_thanks.html rename to cashonly/web/templates/cashonly/web/buy_thanks.html index 9a013ea..82220db 100644 --- a/cashonly/templates/cashonly/buy_thanks.html +++ b/cashonly/web/templates/cashonly/web/buy_thanks.html @@ -1,4 +1,4 @@ -{% extends "cashonly/base.html" %} +{% extends "cashonly/web/base.html" %} {% load i18n %} diff --git a/cashonly/templates/cashonly/debt_reminder.txt b/cashonly/web/templates/cashonly/web/debt_reminder.txt similarity index 100% rename from cashonly/templates/cashonly/debt_reminder.txt rename to cashonly/web/templates/cashonly/web/debt_reminder.txt diff --git a/cashonly/templates/cashonly/includes/product_list.html b/cashonly/web/templates/cashonly/web/includes/product_list.html similarity index 100% rename from cashonly/templates/cashonly/includes/product_list.html rename to cashonly/web/templates/cashonly/web/includes/product_list.html diff --git a/cashonly/templates/cashonly/includes/transaction_list.html b/cashonly/web/templates/cashonly/web/includes/transaction_list.html similarity index 100% rename from cashonly/templates/cashonly/includes/transaction_list.html rename to cashonly/web/templates/cashonly/web/includes/transaction_list.html diff --git a/cashonly/web/templates/cashonly/web/index.html b/cashonly/web/templates/cashonly/web/index.html new file mode 100644 index 0000000..0bb3fc6 --- /dev/null +++ b/cashonly/web/templates/cashonly/web/index.html @@ -0,0 +1 @@ +{% extends "cashonly/web/base.html" %} diff --git a/cashonly/templates/cashonly/login.html b/cashonly/web/templates/cashonly/web/login.html similarity index 100% rename from cashonly/templates/cashonly/login.html rename to cashonly/web/templates/cashonly/web/login.html diff --git a/cashonly/templates/cashonly/product_detail.html b/cashonly/web/templates/cashonly/web/product_detail.html similarity index 89% rename from cashonly/templates/cashonly/product_detail.html rename to cashonly/web/templates/cashonly/web/product_detail.html index 1c6184f..79080f2 100644 --- a/cashonly/templates/cashonly/product_detail.html +++ b/cashonly/web/templates/cashonly/web/product_detail.html @@ -1,4 +1,4 @@ -{% extends "cashonly/base.html" %} +{% extends "cashonly/web/base.html" %} {% load i18n %} diff --git a/cashonly/templates/cashonly/product_list.html b/cashonly/web/templates/cashonly/web/product_list.html similarity index 76% rename from cashonly/templates/cashonly/product_list.html rename to cashonly/web/templates/cashonly/web/product_list.html index 52cee30..d9bad99 100644 --- a/cashonly/templates/cashonly/product_list.html +++ b/cashonly/web/templates/cashonly/web/product_list.html @@ -1,4 +1,4 @@ -{% extends "cashonly/base.html" %} +{% extends "cashonly/web/base.html" %} {% load static %} {% load i18n %} @@ -15,6 +15,6 @@
-{% include "cashonly/includes/product_list.html" with products=product_list only %} +{% include "cashonly/web/includes/product_list.html" with products=product_list only %} {% endblock %} diff --git a/cashonly/templates/cashonly/transaction_list.html b/cashonly/web/templates/cashonly/web/transaction_list.html similarity index 91% rename from cashonly/templates/cashonly/transaction_list.html rename to cashonly/web/templates/cashonly/web/transaction_list.html index ff4710e..8ff5cf0 100644 --- a/cashonly/templates/cashonly/transaction_list.html +++ b/cashonly/web/templates/cashonly/web/transaction_list.html @@ -1,4 +1,4 @@ -{% extends "cashonly/base.html" %} +{% extends "cashonly/web/base.html" %} {% load i18n %} @@ -16,7 +16,7 @@
-{% include "cashonly/includes/transaction_list.html" with transactions=transaction_list only %} +{% include "cashonly/web/includes/transaction_list.html" with transactions=transaction_list only %}