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 @@ |
|||||||
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 @@ |
|||||||
|
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 @@ |
|||||||
|
from django.apps import AppConfig |
||||||
|
|
||||||
|
|
||||||
|
class CashonlyCoreConfig(AppConfig): |
||||||
|
name = 'cashonly.core' |
||||||
|
label = 'cashonly_core' |
@ -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 @@ |
|||||||
|
# -*- 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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
# 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 @@ |
|||||||
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 @@ |
|||||||
{% extends "cashonly/base.html" %} |
|
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
{% extends "cashonly/base.html" %} |
{% extends "cashonly/web/base.html" %} |
||||||
{% load i18n %} |
{% load i18n %} |
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@ |
|||||||
{% extends "cashonly/base.html" %} |
{% extends "cashonly/web/base.html" %} |
||||||
{% load i18n %} |
{% load i18n %} |
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@ |
|||||||
{% extends "cashonly/base.html" %} |
{% extends "cashonly/web/base.html" %} |
||||||
{% load i18n %} |
{% load i18n %} |
||||||
|
|
||||||
|
|
@ -0,0 +1 @@ |
|||||||
|
{% extends "cashonly/web/base.html" %} |
@ -1,4 +1,4 @@ |
|||||||
{% extends "cashonly/base.html" %} |
{% extends "cashonly/web/base.html" %} |
||||||
{% load i18n %} |
{% load i18n %} |
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@ |
|||||||
{% extends "cashonly/base.html" %} |
{% extends "cashonly/web/base.html" %} |
||||||
{% load i18n %} |
{% load i18n %} |
||||||
{% load bootstrap %} |
{% load bootstrap %} |
||||||
{% load staticfiles %} |
{% load staticfiles %} |
@ -1,4 +1,4 @@ |
|||||||
{% extends "cashonly/base.html" %} |
{% extends "cashonly/web/base.html" %} |
||||||
{% load i18n %} |
{% load i18n %} |
||||||
|
|
||||||
{% block content %} |
{% block content %} |
@ -1,5 +1,5 @@ |
|||||||
from django.conf.urls import url |
from django.conf.urls import url |
||||||
from cashonly import views |
from cashonly.web import views |
||||||
|
|
||||||
urlpatterns = [ |
urlpatterns = [ |
||||||
url(r'^$', views.overview, name='overview'), |
url(r'^$', views.overview, name='overview'), |
Loading…
Reference in new issue