Browse Source

pep8

master
Fr3deric 7 years ago
parent
commit
e1598090f7
  1. 232
      cashonly/admin.py
  2. 27
      cashonly/auth.py
  3. 11
      cashonly/formfields.py
  4. 83
      cashonly/management/commands/dailydigest.py
  5. 33
      cashonly/management/commands/debtreminder.py
  6. 336
      cashonly/models.py
  7. 33
      cashonly/urls.py
  8. 4
      cashonly/version.py
  9. 49
      cashonly/views.py

232
cashonly/admin.py

@ -8,141 +8,147 @@ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy from django.utils.translation import ugettext_lazy
from django.utils.translation import ugettext_noop from django.utils.translation import ugettext_noop
class AccountForm(forms.ModelForm): class AccountForm(forms.ModelForm):
credit_change = forms.DecimalField(max_digits=5, decimal_places=2, credit_change = forms.DecimalField(max_digits=5, decimal_places=2,
required=False, required=False,
label=ugettext_lazy('amount')) label=ugettext_lazy('amount'))
credit_change_comment = forms.CharField(max_length=64, required=False,
label=ugettext_lazy('comment'))
credit_change_comment = forms.CharField(max_length=64, required=False, pin_change = DigitField(min_length=4, required=False,
label=ugettext_lazy('comment')) label=ugettext_lazy('PIN'))
#pin_change = forms.RegexField(regex='^\d{4,}$', required=False, pin_empty = forms.BooleanField(required=False,
# label=ugettext_lazy('PIN')) label=ugettext_lazy('clear PIN'))
pin_change = DigitField(min_length=4, required=False,
label=ugettext_lazy('PIN'))
pin_empty = forms.BooleanField(required=False, class Meta:
label=ugettext_lazy('clear PIN')) model = Account
class Meta: # Include all fields (omitting this causes a RemovedInDjango18Warning)
model = Account exclude = []
# Include all fields (omitting this causes a RemovedInDjango18Warning)
exclude = []
class AccountAdmin(admin.ModelAdmin): class AccountAdmin(admin.ModelAdmin):
list_display = ('user', 'card_number', 'credit', 'transaction_link') list_display = ('user', 'card_number', 'credit', 'transaction_link')
form = AccountForm form = AccountForm
readonly_fields = ('user', 'credit',) readonly_fields = ('user', 'credit',)
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('user', 'card_number', 'credit'), 'fields': ('user', 'card_number', 'credit'),
}), }),
(ugettext_lazy('credit change'), { (ugettext_lazy('credit change'), {
'fields': ('credit_change', 'credit_change_comment'), 'fields': ('credit_change', 'credit_change_comment'),
}), }),
(ugettext_lazy('change PIN'), { (ugettext_lazy('change PIN'), {
'fields': ('pin_change', 'pin_empty'), 'fields': ('pin_change', 'pin_empty'),
}), }),
) )
# Disable manual creation of accounts. # Disable manual creation of accounts.
# Accounts are create after user creation automatically. # Accounts are create after user creation automatically.
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False
def transaction_link(self, account): def transaction_link(self, account):
return '<a href="%s?account__id__exact=%d">%s</a>' % \ return '<a href="%s?account__id__exact=%d">%s</a>' % \
(reverse("admin:cashonly_transaction_changelist"), account.id, (reverse("admin:cashonly_transaction_changelist"), account.id,
_('Transactions')) _('Transactions'))
transaction_link.short_description = ugettext_lazy('Transaction link') transaction_link.short_description = ugettext_lazy('Transaction link')
transaction_link.allow_tags = True transaction_link.allow_tags = True
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
pin = form.cleaned_data['pin_change'] pin = form.cleaned_data['pin_change']
pin_empty = form.cleaned_data['pin_empty'] pin_empty = form.cleaned_data['pin_empty']
if pin_empty: if pin_empty:
obj.clear_pin() obj.clear_pin()
else: else:
if pin is not None and len(pin) != 0: if pin is not None and len(pin) != 0:
obj.set_pin(pin) obj.set_pin(pin)
PAYOUT_SUBJECT = ugettext_noop('Payout') PAYOUT_SUBJECT = ugettext_noop('Payout')
DEPOSIT_SUBJECT = ugettext_noop('Deposit') DEPOSIT_SUBJECT = ugettext_noop('Deposit')
DESCRIPTION = ugettext_noop('Authorized by %(first)s %(last)s') DESCRIPTION = ugettext_noop('Authorized by %(first)s %(last)s')
amount = form.cleaned_data['credit_change'] amount = form.cleaned_data['credit_change']
comment = form.cleaned_data['credit_change_comment'] comment = form.cleaned_data['credit_change_comment']
if amount is not None and amount != 0: if amount is not None and amount != 0:
if amount > 0: if amount > 0:
subject = DEPOSIT_SUBJECT subject = DEPOSIT_SUBJECT
else: else:
subject = PAYOUT_SUBJECT subject = PAYOUT_SUBJECT
desc = DESCRIPTION % {'first': request.user.first_name, desc = DESCRIPTION % {'first': request.user.first_name,
'last': request.user.last_name} 'last': request.user.last_name}
if comment is not None and len(comment) > 0: if comment is not None and len(comment) > 0:
desc += ' (%s)' % (comment) desc += ' (%s)' % (comment)
obj.change_credit(amount, subject, desc) obj.change_credit(amount, subject, desc)
# Make sure the object is saved in any case # Make sure the object is saved in any case
obj.save() obj.save()
class ProductBarcodeInline(admin.TabularInline): class ProductBarcodeInline(admin.TabularInline):
model = ProductBarcode model = ProductBarcode
extra = 1 extra = 1
class ProductAdmin(admin.ModelAdmin): class ProductAdmin(admin.ModelAdmin):
list_display = ('name', 'category', 'price', 'active') list_display = ('name', 'category', 'price', 'active')
list_filter = ['category', 'active'] list_filter = ['category', 'active']
ordering = ('-active', 'name') ordering = ('-active', 'name')
inlines = [ProductBarcodeInline] inlines = [ProductBarcodeInline]
fields = ('name', 'price', 'active', 'category', 'image') fields = ('name', 'price', 'active', 'category', 'image')
class ProductCategoryAdmin(admin.ModelAdmin): class ProductCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'comment') list_display = ('name', 'comment')
class SalesLogEntryAdmin(admin.ModelAdmin): class SalesLogEntryAdmin(admin.ModelAdmin):
list_display = ('account', 'timestamp', 'product', 'count', 'unit_price') list_display = ('account', 'timestamp', 'product', 'count', 'unit_price')
list_filter = ['account', 'timestamp', 'product'] list_filter = ['account', 'timestamp', 'product']
# Make sales log entries completely read-only # Make sales log entries completely read-only
readonly_fields = map(lambda f: f.name, SalesLogEntry._meta.fields) readonly_fields = map(lambda f: f.name, SalesLogEntry._meta.fields)
class TransactionAdmin(admin.ModelAdmin): class TransactionAdmin(admin.ModelAdmin):
list_display = ('account', 'timestamp', 'subject', 'description', 'amount') list_display = ('account', 'timestamp', 'subject', 'description', 'amount')
list_filter = ['account', 'timestamp', 'subject'] list_filter = ['account', 'timestamp', 'subject']
# Disable mass deletion in the overview page # Disable mass deletion in the overview page
actions = None actions = None
date_hierarchy = 'timestamp' date_hierarchy = 'timestamp'
# Disable tampering with the transactions completely. # Disable tampering with the transactions completely.
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False
def has_change_permission(self, request, obj=None):
if obj is None: def has_change_permission(self, request, obj=None):
return True if obj is None:
return False return True
def has_delete_permission(self, request, obj=None): return False
return False
def has_delete_permission(self, request, obj=None):
# Needed to not trigger an ImproperlyConfigured exception. return False
# FIXME: a bit too hacky
def changelist_view(self, request, extra_context=None): # Needed to not trigger an ImproperlyConfigured exception.
self.list_display_links = (None, ) # FIXME: a bit too hacky
return super(TransactionAdmin, self).changelist_view(request, def changelist_view(self, request, extra_context=None):
extra_context=None) self.list_display_links = (None, )
return super(TransactionAdmin, self).changelist_view(
request,
extra_context=None,
)
admin.site.register(Account, AccountAdmin) admin.site.register(Account, AccountAdmin)
admin.site.register(Product, ProductAdmin) admin.site.register(Product, ProductAdmin)
#admin.site.register(ProductBarcode)
admin.site.register(ProductCategory, ProductCategoryAdmin) admin.site.register(ProductCategory, ProductCategoryAdmin)
admin.site.register(Transaction, TransactionAdmin) admin.site.register(Transaction, TransactionAdmin)
admin.site.register(SalesLogEntry, SalesLogEntryAdmin) admin.site.register(SalesLogEntry, SalesLogEntryAdmin)

27
cashonly/auth.py

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

11
cashonly/formfields.py

@ -3,11 +3,12 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.validators import EMPTY_VALUES from django.core.validators import EMPTY_VALUES
class DigitField(CharField): class DigitField(CharField):
def clean(self, value): def clean(self, value):
super(DigitField, self).clean(value) super(DigitField, self).clean(value)
if value not in EMPTY_VALUES and not value.isdigit(): if value not in EMPTY_VALUES and not value.isdigit():
raise ValidationError(_('Please enter only digits.')) raise ValidationError(_('Please enter only digits.'))
return value return value

83
cashonly/management/commands/dailydigest.py

@ -13,56 +13,57 @@ import datetime
RANGE = 24 RANGE = 24
USERSETTINGS_URL = 'https://cypher/kasse/usersettings/' USERSETTINGS_URL = 'https://cypher/kasse/usersettings/'
class Command(NoArgsCommand): class Command(NoArgsCommand):
help = 'Sends out the daily digest to all users with transactions' + \ help = 'Sends out the daily digest to all users with transactions' + \
'in the last %dh' % RANGE 'in the last %dh' % RANGE
def handle_noargs(self, **options): def handle_noargs(self, **options):
translation.activate('de') translation.activate('de')
tpl = get_template('cashonly/daily_digest.txt') tpl = get_template('cashonly/daily_digest.txt')
messages = [] messages = []
for a in Account.objects.all(): for a in Account.objects.all():
name = '%s %s' % (a.user.first_name, a.user.last_name) name = '%s %s' % (a.user.first_name, a.user.last_name)
context = {'name': name, context = {'name': name,
'credit': a.credit, 'credit': a.credit,
'range': RANGE, 'range': RANGE,
'url': USERSETTINGS_URL} 'url': USERSETTINGS_URL}
transactions = Transaction.objects.filter(account = a) \ transactions = Transaction.objects.filter(account=a) \
.filter(timestamp__gte=(datetime.datetime.now() .filter(timestamp__gte=(datetime.datetime.now() -
- datetime.timedelta(hours = RANGE))) datetime.timedelta(hours=RANGE)))
if transactions.count() > 0: if transactions.count() > 0:
lengths = {'timestamp': len(_('date')), lengths = {'timestamp': len(_('date')),
'description': len(_('subject')), 'description': len(_('subject')),
'amount': len(_('amount'))} 'amount': len(_('amount'))}
sum = 0 sum = 0
for t in transactions: for t in transactions:
lengths['timestamp'] = \ lengths['timestamp'] = \
max(lengths['timestamp'], len(DateFormat(t.timestamp) \ max(lengths['timestamp'], len(DateFormat(t.timestamp)
.format(get_format('SHORT_DATETIME_FORMAT')))) .format(get_format('SHORT_DATETIME_FORMAT'))))
lengths['description'] = \ lengths['description'] = \
max(lengths['description'], len(t.description)) max(lengths['description'], len(t.description))
lengths['amount'] = \ lengths['amount'] = \
max(lengths['amount'], len(str(t.amount))) max(lengths['amount'], len(str(t.amount)))
t.description = t.description.split('\n') t.description = t.description.split('\n')
sum += t.amount sum += t.amount
lengths['sum'] = lengths['timestamp'] + \ lengths['sum'] = lengths['timestamp'] + \
lengths['description'] + lengths['amount'] lengths['description'] + lengths['amount']
context['lengths'] = lengths context['lengths'] = lengths
context['tl'] = transactions context['tl'] = transactions
context['sum'] = sum context['sum'] = sum
rcpts = ['%s <%s>' % (name, a.user.email)] rcpts = ['%s <%s>' % (name, a.user.email)]
messages.append(('%s%s' % (settings.EMAIL_SUBJECT_PREFIX, messages.append(('%s%s' % (settings.EMAIL_SUBJECT_PREFIX,
_('Account Statement')), _('Account Statement')),
tpl.render(Context(context)), tpl.render(Context(context)),
settings.DEFAULT_FROM_EMAIL, rcpts)) settings.DEFAULT_FROM_EMAIL, rcpts))
send_mass_mail(tuple(messages)) send_mass_mail(tuple(messages))

33
cashonly/management/commands/debtreminder.py

@ -8,25 +8,26 @@ from django.template.loader import get_template
from django.utils import translation from django.utils import translation
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
class Command(NoArgsCommand): class Command(NoArgsCommand):
help = 'Sends a reminder mail to every with a negative credit' help = 'Sends a reminder mail to every with a negative credit'
def handle_noargs(self, **options):
translation.activate('de')
def handle_noargs(self, **options): tpl = get_template('cashonly/debt_reminder.txt')
translation.activate('de')
tpl = get_template('cashonly/debt_reminder.txt') messages = []
for a in Account.objects.all():
if a.credit < 0:
name = '%s %s' % (a.user.first_name, a.user.last_name)
context = {'name': name, 'credit': a.credit}
messages = [] rcpts = ['%s <%s>' % (name, a.user.email)]
for a in Account.objects.all():
if a.credit < 0:
name = '%s %s' % (a.user.first_name, a.user.last_name)
context = {'name': name, 'credit': a.credit}
rcpts = ['%s <%s>' % (name, a.user.email)]
messages.append(('%s%s' % (settings.EMAIL_SUBJECT_PREFIX, messages.append(('%s%s' % (settings.EMAIL_SUBJECT_PREFIX,
_('Debt Reminder')), _('Debt Reminder')),
tpl.render(Context(context)), tpl.render(Context(context)),
settings.DEFAULT_FROM_EMAIL, rcpts)) settings.DEFAULT_FROM_EMAIL, rcpts))
send_mass_mail(tuple(messages)) send_mass_mail(tuple(messages))

336
cashonly/models.py

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

33
cashonly/urls.py

@ -2,29 +2,32 @@ from django.conf.urls import url
from cashonly import views from cashonly import views
urlpatterns = [ urlpatterns = [
url(r'^$', views.overview, name='overview'), url(r'^$', views.overview, name='overview'),
url(r'^product/(?P<pk>\d+)/$', views.ProductView.as_view(), name='product'), url(r'^product/(?P<pk>\d+)/$', views.ProductView.as_view(),
name='product'),
url(r'transactions/$', views.transactions, {'detailed': False, 'page':1}, name='transactions'), url(r'transactions/$', views.transactions, {'detailed': False, 'page': 1},
name='transactions'),
url(r'transactions/(?P<page>\d+)/$', views.transactions, {'detailed': False}, name='transactions'), url(r'transactions/(?P<page>\d+)/$', views.transactions,
{'detailed': False}, name='transactions'),
url(r'transactions/(?P<page>\d+)/detailed/$', views.transactions, {'detailed': True}, url(r'transactions/(?P<page>\d+)/detailed/$', views.transactions,
name='transactions_detailed'), {'detailed': True}, name='transactions_detailed'),
url(r'products/((?P<category_id>\d+)/)?$', views.products, name='products'), url(r'products/((?P<category_id>\d+)/)?$', views.products,
name='products'),
url(r'buy/(?P<product_id>\d+)/$', views.buy, name='buy'), url(r'buy/(?P<product_id>\d+)/$', views.buy, name='buy'),
url(r'buy/(?P<product_id>\d+)/really/$', views.buy, url(r'buy/(?P<product_id>\d+)/really/$', views.buy,
{'confirm': True}, name='buy_really'), {'confirm': True}, name='buy_really'),
url(r'buy/thanks/$', views.buy_thanks, name='buy_thanks'), url(r'buy/thanks/$', views.buy_thanks, name='buy_thanks'),
url(r'buy/error/$', views.buy_error, name='buy_error'), url(r'buy/error/$', views.buy_error, name='buy_error'),
url(r'usersettings(/(?P<submit>\w+))?/$', views.usersettings, url(r'usersettings(/(?P<submit>\w+))?/$', views.usersettings,
name='usersettings'), name='usersettings'),
] ]

4
cashonly/version.py

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

49
cashonly/views.py

@ -9,22 +9,27 @@ from django.utils.translation import ugettext_lazy
import cashonly.version import cashonly.version
import datetime import datetime
def version_number_context_processor(request): def version_number_context_processor(request):
return {'version_number': cashonly.version.CASHONLY_VERSION} return {'version_number': cashonly.version.CASHONLY_VERSION}
@login_required @login_required
def overview(request): def overview(request):
a = request.user.account a = request.user.account
time = datetime.datetime.now() - datetime.timedelta(hours=12) time = datetime.datetime.now() - datetime.timedelta(hours=12)
transactions = Transaction.objects.filter(account=a).filter(timestamp__gte=time).order_by('-timestamp') transactions = Transaction.objects.filter(account=a) \
.filter(timestamp__gte=time).order_by('-timestamp')
# FIXME: distinct doesn't work as expected, so fetch 20 rows and hope that there are 3 distinct products # FIXME: distinct doesn't work as expected, so fetch 20 rows and hope that
purchases = Product.objects.filter(saleslogentry__account=a).order_by('-saleslogentry__timestamp').distinct()[:20] # there are 3 distinct products
purchases = Product.objects.filter(saleslogentry__account=a) \
.order_by('-saleslogentry__timestamp').distinct()[:20]
products = [] products = []
# Find 3 products # Find 3 products
for p in purchases: for p in purchases:
if not p in products: if p not in products:
products.append(p) products.append(p)
if len(products) == 3: if len(products) == 3:
@ -53,8 +58,10 @@ def transactions(request, detailed, page):
except paginator.EmptyPage: except paginator.EmptyPage:
transaction_list = paginator.page(paginator.num_pages) transaction_list = paginator.page(paginator.num_pages)
return render(request, 'cashonly/transaction_list.html', { 'transaction_list': transaction_list, return render(request, 'cashonly/transaction_list.html',
'detailed': detailed }) {'transaction_list': transaction_list,
'detailed': detailed})
def products(request, category_id=None): def products(request, category_id=None):
if category_id is None: if category_id is None:
@ -62,13 +69,15 @@ def products(request, category_id=None):
products = Product.objects.filter(active=True) products = Product.objects.filter(active=True)
else: else:
category = get_object_or_404(ProductCategory, id=category_id) category = get_object_or_404(ProductCategory, id=category_id)
products = Product.objects.filter(active=True).filter(category=category) products = Product.objects.filter(active=True) \
.filter(category=category)
categories = ProductCategory.objects.all() categories = ProductCategory.objects.all()
return render(request, 'cashonly/product_list.html', { 'product_list': products, return render(request, 'cashonly/product_list.html',
'category': category, {'product_list': products, 'category': category,
'categories': categories }) 'categories': categories})
@login_required @login_required
def buy(request, product_id, confirm=False): def buy(request, product_id, confirm=False):
@ -80,20 +89,25 @@ def buy(request, product_id, confirm=False):
else: else:
return redirect('buy_error') return redirect('buy_error')
else: else:
return render(request, 'cashonly/buy_confirm.html', {'product': product}) return render(request, 'cashonly/buy_confirm.html',
{'product': product})
@login_required @login_required
def buy_thanks(request): def buy_thanks(request):
return render(request, 'cashonly/buy_thanks.html') return render(request, 'cashonly/buy_thanks.html')
@login_required @login_required
def buy_error(request): def buy_error(request):
return render(request, 'cashonly/buy_error.html') return render(request, 'cashonly/buy_error.html')
class UserSettingsForm(forms.Form): class UserSettingsForm(forms.Form):
daily_digest = forms.BooleanField(required=False, daily_digest = forms.BooleanField(required=False,
label=ugettext_lazy('daily digest')) label=ugettext_lazy('daily digest'))
class UserPinForm(forms.Form): class UserPinForm(forms.Form):
pin = forms.CharField(max_length=32, widget=forms.PasswordInput, pin = forms.CharField(max_length=32, widget=forms.PasswordInput,
label=ugettext_lazy('PIN'), required=False) label=ugettext_lazy('PIN'), required=False)
@ -104,14 +118,14 @@ class UserPinForm(forms.Form):
def clean(self): def clean(self):
cleaned_data = super(UserPinForm, self).clean() cleaned_data = super(UserPinForm, self).clean()
if not (cleaned_data.has_key('pin') or if 'pin' not in cleaned_data and 'pin_confirm' not in cleaned_data:
cleaned_data.has_key('pin_confirm')):
return cleaned_data return cleaned_data
if cleaned_data['pin'] != cleaned_data['pin_confirm']: if cleaned_data['pin'] != cleaned_data['pin_confirm']:
raise forms.ValidationError(_('PINs do not match.')) raise forms.ValidationError(_('PINs do not match.'))
return cleaned_data return cleaned_data
@login_required @login_required
def usersettings(request, submit=None): def usersettings(request, submit=None):
daily_digest = request.user.account.daily_digest daily_digest = request.user.account.daily_digest
@ -137,10 +151,5 @@ def usersettings(request, submit=None):
request.user.account.save() request.user.account.save()
return render(request, 'cashonly/usersettings_saved.html') return render(request, 'cashonly/usersettings_saved.html')
return render(request, 'cashonly/usersettings.html', { 'settings_form': settings_form, return render(request, 'cashonly/usersettings.html',
'pin_form': pin_form}) {'settings_form': settings_form, 'pin_form': pin_form})

Loading…
Cancel
Save