views.py 16.5 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

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

Skia's avatar
Skia committed
21
class GetUserForm(forms.Form):
22 23 24 25 26 27 28
    """
    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
29
    code = forms.CharField(label="Code", max_length=10, required=False)
30 31
    id = forms.IntegerField(label="ID", required=False)
# TODO: add a nice JS widget to search for users
Skia's avatar
Skia committed
32

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

37 38 39 40 41 42 43 44
    def clean(self):
        cleaned_data = super(GetUserForm, self).clean()
        user = None
        if cleaned_data['code'] != "":
            user = Customer.objects.filter(account_id=cleaned_data['code']).first()
        elif cleaned_data['id'] is not None:
            user = Customer.objects.filter(user=cleaned_data['id']).first()
        if user is None:
Skia's avatar
Skia committed
45
            raise forms.ValidationError(_("User not found"))
46 47 48
        cleaned_data['user_id'] = user.user.id
        return cleaned_data

49 50 51 52 53 54 55
class RefillForm(forms.ModelForm):
    error_css_class = 'error'
    required_css_class = 'required'
    class Meta:
        model = Refilling
        fields = ['amount', 'payment_method', 'bank']

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

Skia's avatar
Skia committed
65
    def get_context_data(self, **kwargs):
Skia's avatar
Skia committed
66
        """
67
        We handle here the login form for the barman
Skia's avatar
Skia committed
68
        """
69 70
        if self.request.method == 'POST':
            self.object = self.get_object()
Skia's avatar
Skia committed
71 72
        kwargs = super(CounterMain, self).get_context_data(**kwargs)
        kwargs['login_form'] = AuthenticationForm()
Skia's avatar
Skia committed
73
        kwargs['login_form'].fields['username'].widget.attrs['autofocus'] = True
Skia's avatar
Skia committed
74
        kwargs['form'] = self.get_form()
Skia's avatar
Skia committed
75 76 77 78
        if self.object.type == 'BAR':
            kwargs['barmen'] = Counter.get_barmen_list(self.object.id)
        elif self.request.user.is_authenticated():
            kwargs['barmen'] = [self.request.user]
79 80 81 82 83
        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
84 85
        return kwargs

86 87 88 89 90 91 92 93 94 95
    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)

96
class CounterClick(DetailView):
Skia's avatar
Skia committed
97 98
    """
    The click view
99 100
    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
101
    """
102
    model = Counter
Skia's avatar
Skia committed
103 104
    template_name = 'counter/counter_click.jinja'
    pk_url_kwarg = "counter_id"
105 106

    def get(self, request, *args, **kwargs):
107
        """Simple get view"""
108
        self.customer = Customer.objects.filter(user__id=self.kwargs['user_id']).first()
109 110 111 112
        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
113
        self.refill_form = None
114
        ret = super(CounterClick, self).get(request, *args, **kwargs)
Skia's avatar
Skia committed
115 116 117 118
        if ((self.object.type != "BAR" and not request.user.is_authenticated()) or
                (self.object.type == "BAR" and
                len(Counter.get_barmen_list(self.object.id)) < 1)): # Check that at least one barman is logged in
            ret = self.cancel(request) # Otherwise, go to main view
119
        return ret
Skia's avatar
Skia committed
120 121

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

        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
145 146
        elif 'refill' in request.POST['action']:
            self.refill(request)
Skia's avatar
Skia committed
147 148
        elif 'code' in request.POST['action']:
            return self.parse_code(request)
149 150 151 152 153 154 155
        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
156
    def is_barman_price(self):
Skia's avatar
Skia committed
157
        if self.object.type == "BAR" and self.customer.user.id in [s.id for s in Counter.get_barmen_list(self.object.id)]:
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
            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']
174
        return total / 100
175

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

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

Skia's avatar
Skia committed
204 205 206
    def parse_code(self, request):
        """Parse the string entered by the barman"""
        string = str(request.POST['code']).upper()
Skia's avatar
Skia committed
207
        if string == _("END"):
Skia's avatar
Skia committed
208
            return self.finish(request)
Skia's avatar
Skia committed
209
        elif string == _("CAN"):
Skia's avatar
Skia committed
210 211 212 213 214 215 216 217 218 219
            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
220
            p = self.object.products.filter(code=code).first()
Skia's avatar
Skia committed
221 222 223 224 225 226
            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)

227 228
    def finish(self, request):
        """ Finish the click session, and validate the basket """
Skia's avatar
Skia committed
229 230 231 232 233 234 235 236 237 238 239 240 241
        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))
                s = Selling(product=p, counter=self.object, unit_price=uprice,
Skia's avatar
Skia committed
242
                       quantity=infos['qty'], seller=self.operator, customer=self.customer)
Skia's avatar
Skia committed
243 244 245 246 247 248 249 250 251 252
                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))
253 254 255 256

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

Skia's avatar
Skia committed
260 261
    def refill(self, request):
        """Refill the customer's account"""
262 263 264
        form = RefillForm(request.POST)
        if form.is_valid():
            form.instance.counter = self.object
Skia's avatar
Skia committed
265
            form.instance.operator = self.operator
266 267 268 269
            form.instance.customer = self.customer
            form.instance.save()
        else:
            self.refill_form = form
Skia's avatar
Skia committed
270

271
    def get_context_data(self, **kwargs):
272
        """ Add customer to the context """
273 274
        kwargs = super(CounterClick, self).get_context_data(**kwargs)
        kwargs['customer'] = self.customer
275
        kwargs['basket_total'] = self.sum_basket(self.request)
276
        kwargs['refill_form'] = self.refill_form or RefillForm()
277 278
        return kwargs

Skia's avatar
Skia committed
279
class CounterLogin(RedirectView):
Skia's avatar
Skia committed
280 281 282 283 284
    """
    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
285
    permanent = False
Skia's avatar
Skia committed
286
    def post(self, request, *args, **kwargs):
Skia's avatar
Skia committed
287 288 289
        """
        Register the logged user as barman for this counter
        """
Skia's avatar
Skia committed
290 291
        self.counter_id = kwargs['counter_id']
        form = AuthenticationForm(request, data=request.POST)
Skia's avatar
Skia committed
292
        if form.is_valid():
Skia's avatar
Skia committed
293
            user = Subscriber.objects.filter(username=form.cleaned_data['username']).first()
Skia's avatar
Skia committed
294 295
            if user.is_subscribed():
                Counter.add_barman(self.counter_id, user.id)
Skia's avatar
Skia committed
296 297 298 299 300 301 302 303 304 305
        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
306 307 308
        """
        Unregister the user from the barman
        """
Skia's avatar
Skia committed
309
        self.counter_id = kwargs['counter_id']
Skia's avatar
Skia committed
310
        Counter.del_barman(self.counter_id, request.POST['user_id'])
Skia's avatar
Skia committed
311 312 313 314
        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
315

316 317
## Counter admin views

Skia's avatar
Skia committed
318 319 320 321 322 323 324
class CounterListView(CanViewMixin, ListView):
    """
    A list view for the admins
    """
    model = Counter
    template_name = 'counter/counter_list.jinja'

325
class CounterEditView(CanEditPropMixin, UpdateView):
Skia's avatar
Skia committed
326
    """
Skia's avatar
Skia committed
327
    Edit a counter's main informations (for the counter's admin)
Skia's avatar
Skia committed
328 329 330 331 332 333 334 335 336
    """
    model = Counter
    form_class = modelform_factory(Counter, fields=['name', 'club', 'type', 'products'],
            widgets={'products':CheckboxSelectMultiple})
    pk_url_kwarg = "counter_id"
    template_name = 'counter/counter_edit.jinja'

class CounterCreateView(CanEditMixin, CreateView):
    """
Skia's avatar
Skia committed
337
    Create a counter (for the admins)
Skia's avatar
Skia committed
338 339 340 341 342 343 344 345
    """
    model = Counter
    form_class = modelform_factory(Counter, fields=['name', 'club', 'type', 'products'],
            widgets={'products':CheckboxSelectMultiple})
    template_name = 'counter/counter_edit.jinja'

class CounterDeleteView(CanEditMixin, DeleteView):
    """
Skia's avatar
Skia committed
346
    Delete a counter (for the admins)
Skia's avatar
Skia committed
347 348 349 350 351
    """
    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
352

Skia's avatar
Skia committed
353 354
# Product management

355
class ProductTypeListView(CanEditPropMixin, ListView):
Skia's avatar
Skia committed
356 357 358 359 360 361
    """
    A list view for the admins
    """
    model = ProductType
    template_name = 'counter/producttype_list.jinja'

362
class ProductTypeCreateView(CanCreateMixin, CreateView):
Skia's avatar
Skia committed
363 364 365 366 367 368 369
    """
    A create view for the admins
    """
    model = ProductType
    fields = ['name', 'description', 'icon']
    template_name = 'core/create.jinja'

370
class ProductTypeEditView(CanEditPropMixin, UpdateView):
Skia's avatar
Skia committed
371 372 373 374 375 376 377 378
    """
    An edit view for the admins
    """
    model = ProductType
    template_name = 'core/edit.jinja'
    fields = ['name', 'description', 'icon']
    pk_url_kwarg = "type_id"

379
class ProductListView(CanEditPropMixin, ListView):
Skia's avatar
Skia committed
380 381 382 383 384 385
    """
    A list view for the admins
    """
    model = Product
    template_name = 'counter/product_list.jinja'

386
class ProductCreateView(CanCreateMixin, CreateView):
Skia's avatar
Skia committed
387 388 389 390
    """
    A create view for the admins
    """
    model = Product
Skia's avatar
Skia committed
391 392 393
    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
394

395
class ProductEditView(CanEditPropMixin, UpdateView):
Skia's avatar
Skia committed
396 397 398 399
    """
    An edit view for the admins
    """
    model = Product
Skia's avatar
Skia committed
400 401
    fields = ['name', 'description', 'product_type', 'code', 'purchase_price',
            'selling_price', 'special_selling_price', 'icon', 'club']
Skia's avatar
Skia committed
402 403 404 405
    pk_url_kwarg = "product_id"
    template_name = 'core/edit.jinja'
    # TODO: add management of the 'counters' ForeignKey

406 407 408 409 410 411 412 413 414 415
# User accounting infos

class UserAccountView(DetailView):
    """
    Display a user's account
    """
    model = Customer
    pk_url_kwarg = "user_id"
    template_name = "counter/user_account.jinja"

416
    def dispatch(self, request, *arg, **kwargs): # Manually validates the rights
417 418
        res = super(UserAccountView, self).dispatch(request, *arg, **kwargs)
        if (self.object.user == request.user
Skia's avatar
Skia committed
419 420
                or request.user.is_in_group(settings.SITH_GROUPS['accounting-admin']['name'])
                or request.user.is_in_group(settings.SITH_GROUPS['root']['name'])):
421 422 423 424 425 426 427 428 429
            return res
        raise PermissionDenied

    def get_context_data(self, **kwargs):
        kwargs = super(UserAccountView, self).get_context_data(**kwargs)
        kwargs['profile'] = self.object.user
        # TODO: add list of month where account has activity
        return kwargs

Skia's avatar
Skia committed
430