Browse Source
- 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 behindmaster
Fr3deric
7 years ago
40 changed files with 532 additions and 754 deletions
@ -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 |
@ -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' |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig |
||||
|
||||
|
||||
class CashonlyCoreConfig(AppConfig): |
||||
name = 'cashonly.core' |
||||
label = 'cashonly_core' |
@ -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'), |
||||
), |
||||
] |
@ -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,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') |
||||
|
||||
|
@ -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) |
||||
|
||||
|
@ -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 € 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" |
@ -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 +0,0 @@
@@ -1 +0,0 @@
|
||||
{% extends "cashonly/base.html" %} |
@ -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) |
@ -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' |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
{% extends "cashonly/base.html" %} |
||||
{% extends "cashonly/web/base.html" %} |
||||
{% load i18n %} |
||||
|
||||
|
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
{% extends "cashonly/base.html" %} |
||||
{% extends "cashonly/web/base.html" %} |
||||
{% load i18n %} |
||||
|
||||
|
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
{% extends "cashonly/base.html" %} |
||||
{% extends "cashonly/web/base.html" %} |
||||
{% load i18n %} |
||||
|
||||
|
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
{% extends "cashonly/web/base.html" %} |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
{% extends "cashonly/base.html" %} |
||||
{% extends "cashonly/web/base.html" %} |
||||
{% load i18n %} |
||||
|
||||
|
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
{% extends "cashonly/base.html" %} |
||||
{% extends "cashonly/web/base.html" %} |
||||
{% load i18n %} |
||||
{% load bootstrap %} |
||||
{% load staticfiles %} |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
{% extends "cashonly/base.html" %} |
||||
{% extends "cashonly/web/base.html" %} |
||||
{% load i18n %} |
||||
|
||||
{% block content %} |
@ -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'), |
Loading…
Reference in new issue