Commit d6138a7a authored by Skia's avatar Skia

Make the etickets

parent a12f9dd5
Pipeline #270 failed with stage
in 19 seconds
......@@ -46,6 +46,14 @@
<h4>{% trans %}Eboutic invoices{% endtrans %}</h4>
{{ monthly(invoices_month) }}
{% endif %}
{% if etickets %}
<h4>{% trans %}Etickets{% endtrans %}</h4>
<ul>
{% for s in etickets %}
<li><a href="{{ url('counter:eticket_pdf', selling_id=s.id) }}">{{ s.quantity }} x {{ s.product.eticket }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% else %}
<p>{% trans %}User has no account{% endtrans %}</p>
{% endif %}
......
......@@ -368,7 +368,6 @@ class UserAccountBase(UserTabsMixin, DetailView):
"""
Base class for UserAccount
"""
model = User
pk_url_kwarg = "user_id"
current_tab = "account"
......@@ -386,7 +385,6 @@ class UserAccountView(UserAccountBase):
"""
Display a user's account
"""
template_name = "core/user_account.jinja"
def expense_by_month(self, obj, calc):
......@@ -406,7 +404,6 @@ class UserAccountView(UserAccountBase):
month
))
i += 1
return stats
def invoices_calc(self, query):
......@@ -432,18 +429,15 @@ class UserAccountView(UserAccountBase):
self.object.customer.refillings,
(lambda q: q.amount)
)
kwargs['etickets'] = self.object.customer.buyings.exclude(product__eticket=None).all()
except:
pass
# TODO: add list of month where account has activity
return kwargs
class UserAccountDetailView(UserAccountBase, YearMixin, MonthMixin):
"""
Display a user's account for month
"""
template_name = "core/user_account_detail.jinja"
def get_context_data(self, **kwargs):
......
......@@ -11,4 +11,5 @@ admin.site.register(Refilling)
admin.site.register(Selling)
admin.site.register(Permanency)
admin.site.register(CashRegisterSummary)
admin.site.register(Eticket)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('counter', '0008_counter_token'),
]
operations = [
migrations.CreateModel(
name='Eticket',
fields=[
('id', models.AutoField(verbose_name='ID', auto_created=True, primary_key=True, serialize=False)),
('banner', models.ImageField(null=True, upload_to='etickets', blank=True)),
('secret', models.CharField(unique=True, verbose_name='secret', max_length=64)),
('product', models.OneToOneField(verbose_name='product', related_name='eticket', to='counter.Product')),
],
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('counter', '0009_eticket'),
]
operations = [
migrations.AddField(
model_name='eticket',
name='event_date',
field=models.DateField(blank=True, verbose_name='event date', null=True),
),
migrations.AddField(
model_name='eticket',
name='event_title',
field=models.CharField(blank=True, max_length=64, verbose_name='event title', null=True),
),
]
......@@ -8,6 +8,8 @@ from django.forms import ValidationError
from datetime import timedelta
import random
import string
import os
import base64
from club.models import Club
from accounting.models import CurrencyField
......@@ -282,6 +284,9 @@ class Selling(models.Model):
def is_owned_by(self, user):
return user.is_owner(self.counter) and self.payment_method != "CARD"
def can_be_viewed_by(self, user):
return user == self.customer.user
def delete(self, *args, **kwargs):
self.customer.amount += self.quantity * self.unit_price
self.customer.save()
......@@ -427,3 +432,34 @@ class CashRegisterSummaryItem(models.Model):
class Meta:
verbose_name = _("cash register summary item")
class Eticket(models.Model):
"""
Eticket can be linked to a product an allows PDF generation
"""
product = models.OneToOneField(Product, related_name='eticket', verbose_name=_("product"))
banner = models.ImageField(upload_to='etickets', null=True, blank=True, verbose_name=_("banner"))
event_date = models.DateField(_('event date'), null=True, blank=True)
event_title = models.CharField(_('event title'), max_length=64, null=True, blank=True)
secret = models.CharField(_('secret'), max_length=64, unique=True)
def __str__(self):
return "%s" % (self.product.name)
def get_absolute_url(self):
return reverse('counter:eticket_list')
def save(self, *args, **kwargs):
if not self.id:
self.secret = base64.b64encode(os.urandom(32))
return super(Eticket, self).save(*args, **kwargs)
def is_owned_by(self, user):
"""
Method to see if that object can be edited by the given user
"""
return user.is_in_group(settings.SITH_GROUPS['counter-admin']['name'])
def get_hash(self, string):
import hashlib, hmac
return hmac.new(bytes(self.secret, 'utf-8'), bytes(string, 'utf-8'), hashlib.sha1).hexdigest()
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Eticket list{% endtrans %}
{% endblock %}
{% block content %}
<p><a href="{{ url('counter:new_eticket') }}">{% trans %}New eticket{% endtrans %}</a></p>
{% if eticket_list %}
<h3>{% trans %}Eticket list{% endtrans %}</h3>
<ul>
{% for t in eticket_list %}
<li><a href="{{ url('counter:edit_eticket', eticket_id=t.id) }}">{{ t }} - {{ t.secret }}</a></li>
{% endfor %}
</ul>
{% else %}
{% trans %}There is no eticket in this website.{% endtrans %}
{% endif %}
{% endblock %}
......@@ -11,6 +11,7 @@ urlpatterns = [
url(r'^(?P<counter_id>[0-9]+)/stats$', CounterStatView.as_view(), name='stats'),
url(r'^(?P<counter_id>[0-9]+)/login$', CounterLogin.as_view(), name='login'),
url(r'^(?P<counter_id>[0-9]+)/logout$', CounterLogout.as_view(), name='logout'),
url(r'^eticket/(?P<selling_id>[0-9]+)/pdf$', EticketPDFView.as_view(), name='eticket_pdf'),
url(r'^admin/(?P<counter_id>[0-9]+)$', CounterEditView.as_view(), name='admin'),
url(r'^admin/(?P<counter_id>[0-9]+)/prop$', CounterEditPropView.as_view(), name='prop_admin'),
url(r'^admin$', CounterListView.as_view(), name='admin_list'),
......@@ -26,6 +27,9 @@ urlpatterns = [
url(r'^admin/producttype/list$', ProductTypeListView.as_view(), name='producttype_list'),
url(r'^admin/producttype/create$', ProductTypeCreateView.as_view(), name='new_producttype'),
url(r'^admin/producttype/(?P<type_id>[0-9]+)$', ProductTypeEditView.as_view(), name='producttype_edit'),
url(r'^admin/eticket/list$', EticketListView.as_view(), name='eticket_list'),
url(r'^admin/eticket/new$', EticketCreateView.as_view(), name='new_eticket'),
url(r'^admin/eticket/(?P<eticket_id>[0-9]+)$', EticketEditView.as_view(), name='edit_eticket'),
url(r'^admin/selling/(?P<selling_id>[0-9]+)/delete$', SellingDeleteView.as_view(), name='selling_delete'),
url(r'^admin/refilling/(?P<refilling_id>[0-9]+)/delete$', RefillingDeleteView.as_view(), name='refilling_delete'),
]
......
......@@ -6,7 +6,7 @@ from django.forms.models import modelform_factory
from django.forms import CheckboxSelectMultiple
from django.core.urlresolvers import reverse_lazy, reverse
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
from django.http import HttpResponseRedirect, HttpResponse
from django.utils import timezone
from django import forms
from django.utils.translation import ugettext_lazy as _
......@@ -20,11 +20,11 @@ from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultip
from ajax_select import make_ajax_form, make_ajax_field
from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin, TabedViewMixin
from core.views.forms import SelectUser, LoginForm
from core.views.forms import SelectUser, LoginForm, SelectDate
from core.models import User
from subscription.models import Subscriber, Subscription
from subscription.views import get_subscriber
from counter.models import Counter, Customer, Product, Selling, Refilling, ProductType, CashRegisterSummary, CashRegisterSummaryItem
from counter.models import Counter, Customer, Product, Selling, Refilling, ProductType, CashRegisterSummary, CashRegisterSummaryItem, Eticket
from accounting.models import CurrencyField
class GetUserForm(forms.Form):
......@@ -464,6 +464,11 @@ class CounterAdminTabsMixin(TabedViewMixin):
'slug': 'invoices_call',
'name': _("Invoices call"),
},
{
'url': reverse_lazy('counter:eticket_list'),
'slug': 'etickets',
'name': _("Etickets"),
},
]
class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
......@@ -940,4 +945,105 @@ class InvoiceCallView(CounterAdminTabsMixin, TemplateView):
)).exclude(selling_sum=None).order_by('-selling_sum')
return kwargs
#).exclude(selling_sum=None).order_by('-selling_sum').all()[:100]
class EticketListView(CounterAdminTabsMixin, CanEditPropMixin, ListView):
"""
A list view for the admins
"""
model = Eticket
template_name = 'counter/eticket_list.jinja'
ordering = ['id']
current_tab = "etickets"
class EticketForm(forms.ModelForm):
class Meta:
model = Eticket
fields = ['product', 'banner', 'event_title', 'event_date']
widgets = {
'event_date': SelectDate,
}
product = AutoCompleteSelectField('products', show_help_text=False, label=_("Product"), required=True)
class EticketCreateView(CounterAdminTabsMixin, CanEditPropMixin, CreateView):
"""
Create an eticket
"""
model = Eticket
template_name = 'core/create.jinja'
form_class = EticketForm
current_tab = "etickets"
class EticketEditView(CounterAdminTabsMixin, CanEditPropMixin, UpdateView):
"""
Edit an eticket
"""
model = Eticket
template_name = 'core/edit.jinja'
form_class = EticketForm
pk_url_kwarg = "eticket_id"
current_tab = "etickets"
class EticketPDFView(CanViewMixin, DetailView):
"""
Display the PDF of an eticket
"""
model = Selling
pk_url_kwarg = "selling_id"
def get(self, request, *args, **kwargs):
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
from reportlab.lib.units import cm
from reportlab.graphics.shapes import Drawing
from reportlab.graphics.barcode.qr import QrCodeWidget
from reportlab.graphics import renderPDF
self.object = self.get_object()
eticket = self.object.product.eticket
user = self.object.customer.user
code = "%s %s %s" % (self.object.customer.user.id, self.object.quantity, self.object.product.id)
code += " " + eticket.get_hash(code)[:8].upper()
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'filename="eticket.pdf"'
p = canvas.Canvas(response)
p.setTitle("Eticket")
im = ImageReader("core/static/core/img/eticket.jpg")
width, height = im.getSize()
size = max(width, height)
width = 8 * cm * width / size
height = 8 * cm * height / size
p.drawImage(im, 10 * cm, 25 * cm, width, height)
if eticket.banner:
im = ImageReader(eticket.banner)
width, height = im.getSize()
size = max(width, height)
width = 6 * cm * width / size
height = 6 * cm * height / size
p.drawImage(im, 1 * cm, 25 * cm, width, height)
if user.profile_pict:
im = ImageReader(user.profile_pict.file)
width, height = im.getSize()
size = max(width, height)
width = 150 * width / size
height = 150 * height / size
p.drawImage(im, 10.5 * cm - width / 2, 16 * cm, width, height)
if eticket.event_title:
p.setFont("Helvetica-Bold", 20)
p.drawCentredString(10.5 * cm, 23.6 * cm, eticket.event_title)
if eticket.event_date:
p.setFont("Helvetica-Bold", 16)
p.drawCentredString(10.5 * cm, 22.6 * cm, eticket.event_date.strftime("%d %b %Y"))
p.setFont("Helvetica-Bold", 14)
p.drawCentredString(10.5 * cm, 15 * cm, user.get_display_name())
p.setFont("Courier-Bold", 14)
qrcode = QrCodeWidget(code)
bounds = qrcode.getBounds()
width = bounds[2] - bounds[0]
height = bounds[3] - bounds[1]
d = Drawing(260, 260, transform=[260./width, 0, 0, 260./height, 0, 0])
d.add(qrcode)
renderPDF.draw(d, p, 10.5 * cm - 130, 6.1 * cm)
p.drawCentredString(10.5 * cm, 6 * cm, code)
p.showPage()
p.save()
return response
......@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-09-29 18:31+0200\n"
"POT-Creation-Date: 2016-10-03 19:24+0200\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Skia <skia@libskia.so>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
......@@ -17,8 +17,8 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: accounting/models.py:36 accounting/models.py:55 accounting/models.py:82
#: accounting/models.py:132 club/models.py:19 counter/models.py:60
#: counter/models.py:85 counter/models.py:120 launderette/models.py:15
#: accounting/models.py:132 club/models.py:19 counter/models.py:62
#: counter/models.py:87 counter/models.py:122 launderette/models.py:15
#: launderette/models.py:60 launderette/models.py:85
msgid "name"
msgstr "nom"
......@@ -64,7 +64,7 @@ msgid "account number"
msgstr "numero de compte"
#: accounting/models.py:58 accounting/models.py:83 club/models.py:145
#: counter/models.py:94 counter/models.py:121
#: counter/models.py:96 counter/models.py:123
msgid "club"
msgstr "club"
......@@ -85,12 +85,12 @@ msgstr "Compte club"
msgid "%(club_account)s on %(bank_account)s"
msgstr "%(club_account)s sur %(bank_account)s"
#: accounting/models.py:130 club/models.py:146 counter/models.py:336
#: accounting/models.py:130 club/models.py:146 counter/models.py:341
#: launderette/models.py:122
msgid "start date"
msgstr "date de début"
#: accounting/models.py:131 club/models.py:147 counter/models.py:337
#: accounting/models.py:131 club/models.py:147 counter/models.py:342
msgid "end date"
msgstr "date de fin"
......@@ -102,8 +102,8 @@ msgstr "est fermé"
msgid "club account"
msgstr "compte club"
#: accounting/models.py:135 accounting/models.py:178 counter/models.py:25
#: counter/models.py:224
#: accounting/models.py:135 accounting/models.py:178 counter/models.py:27
#: counter/models.py:226
msgid "amount"
msgstr "montant"
......@@ -124,16 +124,16 @@ msgid "journal"
msgstr "classeur"
#: accounting/models.py:179 core/models.py:479 core/models.py:757
#: counter/models.py:227 counter/models.py:270 counter/models.py:353
#: counter/models.py:229 counter/models.py:272 counter/models.py:358
#: eboutic/models.py:15 eboutic/models.py:48
msgid "date"
msgstr "date"
#: accounting/models.py:180 counter/models.py:354
#: accounting/models.py:180 counter/models.py:359
msgid "comment"
msgstr "commentaire"
#: accounting/models.py:181 counter/models.py:228 counter/models.py:271
#: accounting/models.py:181 counter/models.py:230 counter/models.py:273
#: subscription/models.py:57
msgid "payment method"
msgstr "méthode de paiement"
......@@ -225,7 +225,7 @@ msgstr ""
"Vous devez fournir soit un type comptable simplifié ou un type comptable "
"standard"
#: accounting/models.py:277 counter/models.py:89
#: accounting/models.py:277 counter/models.py:91
msgid "code"
msgstr "code"
......@@ -233,7 +233,7 @@ msgstr "code"
msgid "An accounting type code contains only numbers"
msgstr "Un code comptable ne contient que des numéros"
#: accounting/models.py:282 accounting/models.py:308 counter/models.py:262
#: accounting/models.py:282 accounting/models.py:308 counter/models.py:264
msgid "label"
msgstr "intitulé"
......@@ -485,7 +485,7 @@ msgid "Done"
msgstr "Effectué"
#: accounting/templates/accounting/journal_details.jinja:34
#: counter/templates/counter/cash_summary_list.jinja:32 counter/views.py:697
#: counter/templates/counter/cash_summary_list.jinja:32 counter/views.py:702
msgid "Comment"
msgstr "Commentaire"
......@@ -554,7 +554,7 @@ msgstr "Vous ne pouvez pas faire de boucles dans les clubs"
msgid "A club with that unix_name already exists"
msgstr "Un club avec ce nom UNIX existe déjà."
#: club/models.py:144 counter/models.py:334 counter/models.py:351
#: club/models.py:144 counter/models.py:339 counter/models.py:356
#: eboutic/models.py:14 eboutic/models.py:47 launderette/models.py:89
#: launderette/models.py:126
msgid "user"
......@@ -564,8 +564,8 @@ msgstr "nom d'utilisateur"
msgid "role"
msgstr "rôle"
#: club/models.py:150 core/models.py:32 counter/models.py:61
#: counter/models.py:86
#: club/models.py:150 core/models.py:32 counter/models.py:63
#: counter/models.py:88
msgid "description"
msgstr "description"
......@@ -581,8 +581,7 @@ msgstr "L'utilisateur est déjà membre de ce club"
msgid "past member"
msgstr "Anciens membres"
#: club/templates/club/club_list.jinja:4
#: club/templates/club/club_list.jinja:24
#: club/templates/club/club_list.jinja:4 club/templates/club/club_list.jinja:24
msgid "Club list"
msgstr "Liste des clubs"
......@@ -760,6 +759,7 @@ msgid "End date"
msgstr "Date de fin"
#: club/views.py:178 core/templates/core/user_stats.jinja:27
#: counter/views.py:964
msgid "Product"
msgstr "Produit"
......@@ -1371,13 +1371,11 @@ msgstr "login"
msgid "Lost password?"
msgstr "Mot de passe perdu ?"
#: core/templates/core/macros.jinja:27
#: core/templates/core/user_detail.jinja:27
#: core/templates/core/macros.jinja:27 core/templates/core/user_detail.jinja:27
msgid "Born: "
msgstr "Né le : "
#: core/templates/core/macros.jinja:31
#: core/templates/core/user_detail.jinja:48
#: core/templates/core/macros.jinja:31 core/templates/core/user_detail.jinja:48
msgid "Promo: "
msgstr "Promo : "
......@@ -1588,8 +1586,7 @@ msgstr "Résultat de la recherche"
msgid "Users"
msgstr "Utilisateurs"
#: core/templates/core/search.jinja:18
#: counter/templates/counter/stats.jinja:17
#: core/templates/core/search.jinja:18 counter/templates/counter/stats.jinja:17
msgid "Clubs"
msgstr "Clubs"
......@@ -1629,7 +1626,11 @@ msgstr "Achat sur compte utilisateur"
msgid "Eboutic invoices"
msgstr "Facture eboutic"
#: core/templates/core/user_account.jinja:50
#: core/templates/core/user_account.jinja:50 counter/views.py:470
msgid "Etickets"
msgstr ""
#: core/templates/core/user_account.jinja:58
#: core/templates/core/user_account_detail.jinja:103
msgid "User has no account"
msgstr "L'utilisateur n'a pas de compte"
......@@ -1790,7 +1791,7 @@ msgid "Subscriptions"
msgstr "Cotisations"
#: core/templates/core/user_tools.jinja:23 counter/views.py:440
#: counter/views.py:584
#: counter/views.py:589
msgid "Counters"
msgstr "Comptoirs"
......@@ -1888,83 +1889,83 @@ msgstr "Fillot"
msgid "User already has a profile picture"
msgstr "L'utilisateur a déjà une photo de profil"
#: counter/models.py:24
#: counter/models.py:26
msgid "account id"
msgstr "numéro de compte"
#: counter/models.py:28
#: counter/models.py:30
msgid "customer"
msgstr "client"
#: counter/models.py:29
#: counter/models.py:31
msgid "customers"
msgstr "clients"
#: counter/models.py:44 counter/templates/counter/counter_click.jinja:46
#: counter/models.py:46 counter/templates/counter/counter_click.jinja:46
msgid "Not enough money"
msgstr "Solde insuffisant"
#: counter/models.py:65 counter/models.py:87
#: counter/models.py:67 counter/models.py:89
msgid "product type"
msgstr "type du produit"
#: counter/models.py:90
#: counter/models.py:92
msgid "purchase price"
msgstr "prix d'achat"
#: counter/models.py:91
#: counter/models.py:93
msgid "selling price"
msgstr "prix de vente"
#: counter/models.py:92
#: counter/models.py:94
msgid "special selling price"
msgstr "prix de vente spécial"
#: counter/models.py:93
#: counter/models.py:95
msgid "icon"
msgstr "icône"
#: counter/models.py:95
#: counter/models.py:97
msgid "limit age"
msgstr "âge limite"
#: counter/models.py:96
#: counter/models.py:98
msgid "tray price"
msgstr "prix plateau"
#: counter/models.py:97
#: counter/models.py:99
msgid "parent product"
msgstr "produit parent"
#: counter/models.py:99
#: counter/models.py:101
msgid "buying groups"
msgstr "groupe d'achat"
#: counter/models.py:100
#: counter/models.py:102
msgid "archived"
msgstr "archivé"
#: counter/models.py:103
#: counter/models.py:105 counter/models.py:439
msgid "product"
msgstr "produit"
#: counter/models.py:122
#: counter/models.py:124
msgid "products"
msgstr "produits"
#: counter/models.py:123
#: counter/models.py:125
msgid "counter type"
msgstr "type de comptoir"