views.py 16 KB
Newer Older
Skia's avatar
Skia committed
1
from django.shortcuts import render
Skia's avatar
Skia committed
2
from django.views.generic import ListView, DetailView, RedirectView
Skia's avatar
Skia committed
3
from django.views.generic.edit import UpdateView, CreateView, DeleteView, ProcessFormView, FormMixin
Skia's avatar
Skia committed
4
5
6
from django.forms.models import modelform_factory
from django.forms import CheckboxSelectMultiple
from django.core.urlresolvers import reverse_lazy
Skia's avatar
Skia committed
7
from django.contrib.auth.forms import AuthenticationForm
8
from django.http import HttpResponseRedirect
Skia's avatar
Skia committed
9
from django.utils import timezone
Skia's avatar
Skia committed
10
from django import forms
Skia's avatar
Skia committed
11
from django.utils.translation import ugettext_lazy as _
12
from django.conf import settings
Skia's avatar
Skia committed
13
from django.db import DataError, transaction
Skia's avatar
Skia committed
14

Skia's avatar
Skia committed
15
import re
Skia's avatar
Skia committed
16
from datetime import date, timedelta
Skia's avatar
Skia committed
17

18
from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin
Skia's avatar
Skia committed
19
from subscription.models import Subscriber
Skia's avatar
Skia committed
20
from subscription.views import get_subscriber
Skia's avatar
Skia committed
21
from counter.models import Counter, Customer, Product, Selling, Refilling, ProductType
Skia's avatar
Skia committed
22

Skia's avatar
Skia committed
23
class GetUserForm(forms.Form):
24
25
26
27
28
29
30
    """
    The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view,
    reverse function, or any other use.

    The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with
    some nickname, first name, or last name (TODO)
    """
Skia's avatar
Skia committed
31
    code = forms.CharField(label="Code", max_length=10, required=False)
32
    id = forms.IntegerField(label="ID", required=False)
Skia's avatar
Skia committed
33
    # TODO: add a nice JS widget to search for users
Skia's avatar
Skia committed
34

Skia's avatar
Skia committed
35
36
37
38
    def as_p(self):
        self.fields['code'].widget.attrs['autofocus'] = True
        return super(GetUserForm, self).as_p()

39
40
    def clean(self):
        cleaned_data = super(GetUserForm, self).clean()
Skia's avatar
Skia committed
41
        cus = None
42
        if cleaned_data['code'] != "":
Skia's avatar
Skia committed
43
            cus = Customer.objects.filter(account_id=cleaned_data['code']).first()
44
        elif cleaned_data['id'] is not None:
Skia's avatar
Skia committed
45
46
            cus = Customer.objects.filter(user=cleaned_data['id']).first()
        sub = get_subscriber(cus.user) if cus is not None else None
47
48
        if (cus is None or sub is None or not sub.subscriptions.last() or
            (date.today() - sub.subscriptions.last().subscription_end) > timedelta(days=90)):
Skia's avatar
Skia committed
49
            raise forms.ValidationError(_("User not found"))
Skia's avatar
Skia committed
50
51
        cleaned_data['user_id'] = cus.user.id
        cleaned_data['user'] = cus.user
52
53
        return cleaned_data

54
55
56
57
58
59
60
class RefillForm(forms.ModelForm):
    error_css_class = 'error'
    required_css_class = 'required'
    class Meta:
        model = Refilling
        fields = ['amount', 'payment_method', 'bank']

61
class CounterMain(DetailView, ProcessFormView, FormMixin):
Skia's avatar
Skia committed
62
63
64
    """
    The public (barman) view
    """
Skia's avatar
Skia committed
65
    model = Counter
Skia's avatar
Skia committed
66
    template_name = 'counter/counter_main.jinja'
Skia's avatar
Skia committed
67
    pk_url_kwarg = "counter_id"
68
    form_class = GetUserForm # Form to enter a client code and get the corresponding user id
Skia's avatar
Skia committed
69

Skia's avatar
Skia committed
70
    def get_context_data(self, **kwargs):
Skia's avatar
Skia committed
71
        """
72
        We handle here the login form for the barman
Skia's avatar
Skia committed
73
        """
74
75
        if self.request.method == 'POST':
            self.object = self.get_object()
Skia's avatar
Skia committed
76
77
        kwargs = super(CounterMain, self).get_context_data(**kwargs)
        kwargs['login_form'] = AuthenticationForm()
Skia's avatar
Skia committed
78
        kwargs['login_form'].fields['username'].widget.attrs['autofocus'] = True
Skia's avatar
Skia committed
79
        kwargs['form'] = self.get_form()
Skia's avatar
Skia committed
80
        if self.object.type == 'BAR':
Skia's avatar
Skia committed
81
            kwargs['barmen'] = self.object.get_barmen_list()
Skia's avatar
Skia committed
82
83
        elif self.request.user.is_authenticated():
            kwargs['barmen'] = [self.request.user]
84
85
86
87
88
        if 'last_basket' in self.request.session.keys():
            kwargs['last_basket'] = self.request.session.pop('last_basket')
            kwargs['last_customer'] = self.request.session.pop('last_customer')
            kwargs['last_total'] = self.request.session.pop('last_total')
            kwargs['new_customer_amount'] = self.request.session.pop('new_customer_amount')
Skia's avatar
Skia committed
89
90
        return kwargs

91
92
93
94
95
96
97
98
99
100
    def form_valid(self, form):
        """
        We handle here the redirection, passing the user id of the asked customer
        """
        self.kwargs['user_id'] = form.cleaned_data['user_id']
        return super(CounterMain, self).form_valid(form)

    def get_success_url(self):
        return reverse_lazy('counter:click', args=self.args, kwargs=self.kwargs)

101
class CounterClick(DetailView):
Skia's avatar
Skia committed
102
103
    """
    The click view
104
105
    This is a detail view not to have to worry about loading the counter
    Everything is made by hand in the post method
Skia's avatar
Skia committed
106
    """
107
    model = Counter
Skia's avatar
Skia committed
108
109
    template_name = 'counter/counter_click.jinja'
    pk_url_kwarg = "counter_id"
110
111

    def get(self, request, *args, **kwargs):
112
        """Simple get view"""
113
        self.customer = Customer.objects.filter(user__id=self.kwargs['user_id']).first()
114
115
116
117
        if 'basket' not in request.session.keys(): # Init the basket session entry
            request.session['basket'] = {}
            request.session['basket_total'] = 0
        request.session['not_enough'] = False
118
        self.refill_form = None
119
        ret = super(CounterClick, self).get(request, *args, **kwargs)
Skia's avatar
Skia committed
120
121
        if ((self.object.type != "BAR" and not request.user.is_authenticated()) or
                (self.object.type == "BAR" and
Skia's avatar
Skia committed
122
                len(self.object.get_barmen_list()) < 1)): # Check that at least one barman is logged in
Skia's avatar
Skia committed
123
            ret = self.cancel(request) # Otherwise, go to main view
124
        return ret
Skia's avatar
Skia committed
125
126

    def post(self, request, *args, **kwargs):
127
        """ Handle the many possibilities of the post request """
128
129
        self.object = self.get_object()
        self.customer = Customer.objects.filter(user__id=self.kwargs['user_id']).first()
130
        self.refill_form = None
Skia's avatar
Skia committed
131
132
        if ((self.object.type != "BAR" and not request.user.is_authenticated()) or
                (self.object.type == "BAR" and
Skia's avatar
Skia committed
133
                len(self.object.get_barmen_list()) < 1)): # Check that at least one barman is logged in
134
            return self.cancel(request)
135
136
        if 'basket' not in request.session.keys():
            request.session['basket'] = {}
137
138
            request.session['basket_total'] = 0
        request.session['not_enough'] = False
Skia's avatar
Skia committed
139
140
141
142
143
        if self.object.type != "BAR":
            self.operator = request.user
        elif self.is_barman_price():
            self.operator = self.customer.user
        else:
Skia's avatar
Skia committed
144
            self.operator = self.object.get_random_barman()
145
146
147
148
149

        if 'add_product' in request.POST['action']:
            self.add_product(request)
        elif 'del_product' in request.POST['action']:
            self.del_product(request)
Skia's avatar
Skia committed
150
151
        elif 'refill' in request.POST['action']:
            self.refill(request)
Skia's avatar
Skia committed
152
153
        elif 'code' in request.POST['action']:
            return self.parse_code(request)
154
155
156
157
158
159
160
        elif 'cancel' in request.POST['action']:
            return self.cancel(request)
        elif 'finish' in request.POST['action']:
            return self.finish(request)
        context = self.get_context_data(object=self.object)
        return self.render_to_response(context)

Skia's avatar
Skia committed
161
    def is_barman_price(self):
Skia's avatar
Skia committed
162
        if self.object.type == "BAR" and self.customer.user.id in [s.id for s in self.object.get_barmen_list()]:
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
            return True
        else:
            return False

    def get_price(self, pid):
        p = Product.objects.filter(pk=pid).first()
        if self.is_barman_price():
            price = p.special_selling_price
        else:
            price = p.selling_price
        return price

    def sum_basket(self, request):
        total = 0
        for pid,infos in request.session['basket'].items():
            total += infos['price'] * infos['qty']
179
        return total / 100
180

Skia's avatar
Skia committed
181
    def add_product(self, request, q = 1, p=None):
182
        """ Add a product to the basket """
Skia's avatar
Skia committed
183
184
        pid = p or request.POST['product_id']
        pid = str(pid)
185
        price = self.get_price(pid)
186
        total = self.sum_basket(request)
Skia's avatar
Skia committed
187
        if self.customer.amount < (total + q*float(price)):
188
            request.session['not_enough'] = True
Skia's avatar
Skia committed
189
            return False
190
        if pid in request.session['basket']:
191
            request.session['basket'][pid]['qty'] += q
192
        else:
193
            request.session['basket'][pid] = {'qty': q, 'price': int(price*100)}
Skia's avatar
Skia committed
194
        request.session['not_enough'] = False # Reset not_enough to save the session
195
        request.session.modified = True
Skia's avatar
Skia committed
196
        return True
197
198
199

    def del_product(self, request):
        """ Delete a product from the basket """
200
201
        pid = str(request.POST['product_id'])
        if pid in request.session['basket']:
202
203
            request.session['basket'][pid]['qty'] -= 1
            if request.session['basket'][pid]['qty'] <= 0:
204
                del request.session['basket'][pid]
205
        else:
206
            request.session['basket'][pid] = 0
207
208
        request.session.modified = True

Skia's avatar
Skia committed
209
210
211
    def parse_code(self, request):
        """Parse the string entered by the barman"""
        string = str(request.POST['code']).upper()
Skia's avatar
Skia committed
212
        if string == _("END"):
Skia's avatar
Skia committed
213
            return self.finish(request)
Skia's avatar
Skia committed
214
        elif string == _("CAN"):
Skia's avatar
Skia committed
215
216
217
218
219
220
221
222
223
224
            return self.cancel(request)
        regex = re.compile(r"^((?P<nb>[0-9]+)X)?(?P<code>[A-Z0-9]+)$")
        m = regex.match(string)
        if m is not None:
            nb = m.group('nb')
            code = m.group('code')
            if nb is None:
                nb = 1
            else:
                nb = int(nb)
Skia's avatar
Skia committed
225
            p = self.object.products.filter(code=code).first()
Skia's avatar
Skia committed
226
227
228
229
230
231
            if p is not None:
                while nb > 0 and not self.add_product(request, nb, p.id):
                    nb -= 1
        context = self.get_context_data(object=self.object)
        return self.render_to_response(context)

232
233
    def finish(self, request):
        """ Finish the click session, and validate the basket """
Skia's avatar
Skia committed
234
235
236
237
238
239
240
241
242
243
244
245
        with transaction.atomic():
            request.session['last_basket'] = []
            for pid,infos in request.session['basket'].items():
                # This duplicates code for DB optimization (prevent to load many times the same object)
                p = Product.objects.filter(pk=pid).first()
                if self.is_barman_price():
                    uprice = p.special_selling_price
                else:
                    uprice = p.selling_price
                if uprice * infos['qty'] > self.customer.amount:
                    raise DataError(_("You have not enough money to buy all the basket"))
                request.session['last_basket'].append("%d x %s" % (infos['qty'], p.name))
246
                s = Selling(label=p.name, product=p, club=p.club, counter=self.object, unit_price=uprice,
Skia's avatar
Skia committed
247
                       quantity=infos['qty'], seller=self.operator, customer=self.customer)
Skia's avatar
Skia committed
248
249
250
251
252
253
254
255
256
257
                s.save()
            request.session['last_customer'] = self.customer.user.get_display_name()
            request.session['last_total'] = "%0.2f" % self.sum_basket(request)
            request.session['new_customer_amount'] = str(self.customer.amount)
            del request.session['basket']
            request.session.modified = True
            kwargs = {
                    'counter_id': self.object.id,
                    }
            return HttpResponseRedirect(reverse_lazy('counter:details', args=self.args, kwargs=kwargs))
258
259
260
261

    def cancel(self, request):
        """ Cancel the click session """
        kwargs = {'counter_id': self.object.id}
262
        request.session.pop('basket', None)
263
        return HttpResponseRedirect(reverse_lazy('counter:details', args=self.args, kwargs=kwargs))
264

Skia's avatar
Skia committed
265
266
    def refill(self, request):
        """Refill the customer's account"""
267
268
269
        form = RefillForm(request.POST)
        if form.is_valid():
            form.instance.counter = self.object
Skia's avatar
Skia committed
270
            form.instance.operator = self.operator
271
272
273
274
            form.instance.customer = self.customer
            form.instance.save()
        else:
            self.refill_form = form
Skia's avatar
Skia committed
275

276
    def get_context_data(self, **kwargs):
277
        """ Add customer to the context """
278
279
        kwargs = super(CounterClick, self).get_context_data(**kwargs)
        kwargs['customer'] = self.customer
280
        kwargs['basket_total'] = self.sum_basket(self.request)
281
        kwargs['refill_form'] = self.refill_form or RefillForm()
282
283
        return kwargs

Skia's avatar
Skia committed
284
class CounterLogin(RedirectView):
Skia's avatar
Skia committed
285
286
287
288
289
    """
    Handle the login of a barman

    Logged barmen are stored in the class-wide variable 'barmen_session', in the Counter model
    """
Skia's avatar
Skia committed
290
    permanent = False
Skia's avatar
Skia committed
291
    def post(self, request, *args, **kwargs):
Skia's avatar
Skia committed
292
293
294
        """
        Register the logged user as barman for this counter
        """
Skia's avatar
Skia committed
295
296
        self.counter_id = kwargs['counter_id']
        form = AuthenticationForm(request, data=request.POST)
Skia's avatar
Skia committed
297
        if form.is_valid():
Skia's avatar
Skia committed
298
            user = Subscriber.objects.filter(username=form.cleaned_data['username']).first()
Skia's avatar
Skia committed
299
300
            if user.is_subscribed():
                Counter.add_barman(self.counter_id, user.id)
Skia's avatar
Skia committed
301
302
303
304
305
306
307
308
309
310
        else:
            print("Error logging the barman") # TODO handle that nicely
        return super(CounterLogin, self).post(request, *args, **kwargs)

    def get_redirect_url(self, *args, **kwargs):
        return reverse_lazy('counter:details', args=args, kwargs=kwargs)

class CounterLogout(RedirectView):
    permanent = False
    def post(self, request, *args, **kwargs):
Skia's avatar
Skia committed
311
312
313
        """
        Unregister the user from the barman
        """
Skia's avatar
Skia committed
314
        self.counter_id = kwargs['counter_id']
Skia's avatar
Skia committed
315
        Counter.del_barman(self.counter_id, request.POST['user_id'])
Skia's avatar
Skia committed
316
317
318
319
        return super(CounterLogout, self).post(request, *args, **kwargs)

    def get_redirect_url(self, *args, **kwargs):
        return reverse_lazy('counter:details', args=args, kwargs=kwargs)
Skia's avatar
Skia committed
320

321
322
## Counter admin views

Skia's avatar
Skia committed
323
324
325
326
327
328
329
class CounterListView(CanViewMixin, ListView):
    """
    A list view for the admins
    """
    model = Counter
    template_name = 'counter/counter_list.jinja'

330
class CounterEditView(CanEditPropMixin, UpdateView):
Skia's avatar
Skia committed
331
    """
Skia's avatar
Skia committed
332
    Edit a counter's main informations (for the counter's admin)
Skia's avatar
Skia committed
333
334
    """
    model = Counter
335
336
337
338
    form_class = modelform_factory(Counter, fields=['name', 'club', 'type', 'sellers', 'products'],
            widgets={
                'products':CheckboxSelectMultiple,
                'sellers':CheckboxSelectMultiple})
Skia's avatar
Skia committed
339
    pk_url_kwarg = "counter_id"
340
    template_name = 'core/edit.jinja'
Skia's avatar
Skia committed
341
342
343

class CounterCreateView(CanEditMixin, CreateView):
    """
Skia's avatar
Skia committed
344
    Create a counter (for the admins)
Skia's avatar
Skia committed
345
346
347
348
    """
    model = Counter
    form_class = modelform_factory(Counter, fields=['name', 'club', 'type', 'products'],
            widgets={'products':CheckboxSelectMultiple})
349
    template_name = 'core/create.jinja'
Skia's avatar
Skia committed
350
351
352

class CounterDeleteView(CanEditMixin, DeleteView):
    """
Skia's avatar
Skia committed
353
    Delete a counter (for the admins)
Skia's avatar
Skia committed
354
355
356
357
358
    """
    model = Counter
    pk_url_kwarg = "counter_id"
    template_name = 'core/delete_confirm.jinja'
    success_url = reverse_lazy('counter:admin_list')
Skia's avatar
Skia committed
359

Skia's avatar
Skia committed
360
361
# Product management

362
class ProductTypeListView(CanEditPropMixin, ListView):
Skia's avatar
Skia committed
363
364
365
366
367
368
    """
    A list view for the admins
    """
    model = ProductType
    template_name = 'counter/producttype_list.jinja'

369
class ProductTypeCreateView(CanCreateMixin, CreateView):
Skia's avatar
Skia committed
370
371
372
373
374
375
376
    """
    A create view for the admins
    """
    model = ProductType
    fields = ['name', 'description', 'icon']
    template_name = 'core/create.jinja'

377
class ProductTypeEditView(CanEditPropMixin, UpdateView):
Skia's avatar
Skia committed
378
379
380
381
382
383
384
385
    """
    An edit view for the admins
    """
    model = ProductType
    template_name = 'core/edit.jinja'
    fields = ['name', 'description', 'icon']
    pk_url_kwarg = "type_id"

386
class ProductListView(CanEditPropMixin, ListView):
Skia's avatar
Skia committed
387
388
389
390
391
392
    """
    A list view for the admins
    """
    model = Product
    template_name = 'counter/product_list.jinja'

393
class ProductCreateView(CanCreateMixin, CreateView):
Skia's avatar
Skia committed
394
395
396
397
    """
    A create view for the admins
    """
    model = Product
Skia's avatar
Skia committed
398
399
400
    fields = ['name', 'description', 'product_type', 'code', 'purchase_price',
            'selling_price', 'special_selling_price', 'icon', 'club']
    template_name = 'core/create.jinja'
Skia's avatar
Skia committed
401

402
class ProductEditView(CanEditPropMixin, UpdateView):
Skia's avatar
Skia committed
403
404
405
406
    """
    An edit view for the admins
    """
    model = Product
Skia's avatar
Skia committed
407
408
    fields = ['name', 'description', 'product_type', 'code', 'purchase_price',
            'selling_price', 'special_selling_price', 'icon', 'club']
Skia's avatar
Skia committed
409
410
411
412
    pk_url_kwarg = "product_id"
    template_name = 'core/edit.jinja'
    # TODO: add management of the 'counters' ForeignKey