views.py 51.9 KB
Newer Older
1 2
from django.shortcuts import render, get_object_or_404
from django.http import Http404
Skia's avatar
Skia committed
3
from django.core.exceptions import PermissionDenied
Skia's avatar
Skia committed
4
from django.views.generic import ListView, DetailView, RedirectView, TemplateView
Sli's avatar
Sli committed
5
from django.views.generic.base import View
Skia's avatar
Skia committed
6
from django.views.generic.edit import UpdateView, CreateView, DeleteView, ProcessFormView, FormMixin
Skia's avatar
Skia committed
7 8
from django.forms.models import modelform_factory
from django.forms import CheckboxSelectMultiple
Skia's avatar
Skia committed
9
from django.core.urlresolvers import reverse_lazy, reverse
10
from django.core.exceptions import PermissionDenied
Skia's avatar
Skia committed
11
from django.http import HttpResponseRedirect, HttpResponse
Skia's avatar
Skia committed
12
from django.utils import timezone
Skia's avatar
Skia committed
13
from django import forms
Skia's avatar
Skia committed
14
from django.utils.translation import ugettext_lazy as _
15
from django.conf import settings
Skia's avatar
Skia committed
16
from django.db import DataError, transaction, models
Skia's avatar
Skia committed
17

Skia's avatar
Skia committed
18
import re
Skia's avatar
Skia committed
19 20
import pytz
from datetime import date, timedelta, datetime
21 22
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
from ajax_select import make_ajax_form, make_ajax_field
Skia's avatar
Skia committed
23

24
from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin, TabedViewMixin
25
from core.views.forms import SelectUser, LoginForm, SelectDate, SelectDateTime
26
from core.models import User
Skia's avatar
Skia committed
27
from subscription.models import Subscription
Skia's avatar
Skia committed
28 29
from counter.models import Counter, Customer, Product, Selling, Refilling, ProductType, \
        CashRegisterSummary, CashRegisterSummaryItem, Eticket, Permanency
Skia's avatar
Skia committed
30
from accounting.models import CurrencyField
Skia's avatar
Skia committed
31

Sli's avatar
Sli committed
32
class CounterAdminMixin(View):
Sli's avatar
Sli committed
33 34 35
    """
    This view is made to protect counter admin section
    """
Sli's avatar
Sli committed
36 37 38
    edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID]
    edit_club = []

Sli's avatar
Sli committed
39
    def _test_group(self, user):
Sli's avatar
Sli committed
40
        for g in self.edit_group:
Sli's avatar
Sli committed
41 42 43 44
            if user.is_in_group(g):
                return True
        return False

Sli's avatar
Sli committed
45 46 47 48 49 50 51
    def _test_club(self, user):
        for c in self.edit_club:
            if c.can_be_edited_by(user):
                return True
        return False


Sli's avatar
Sli committed
52
    def dispatch(self, request, *args, **kwargs):
Sli's avatar
Sli committed
53
        res = super(CounterAdminMixin, self).dispatch(request, *args, **kwargs)
Sli's avatar
Sli committed
54 55
        if not (request.user.is_root or self._test_group(request.user)
                or self._test_club(request.user)):
Sli's avatar
Sli committed
56 57 58
            raise PermissionDenied
        return res

Skia's avatar
Skia committed
59
class GetUserForm(forms.Form):
60 61 62 63 64 65 66
    """
    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
67
    code = forms.CharField(label="Code", max_length=10, required=False)
68
    id = AutoCompleteSelectField('users', required=False, label=_("Select user"), help_text=None)
Skia's avatar
Skia committed
69

Skia's avatar
Skia committed
70 71 72 73
    def as_p(self):
        self.fields['code'].widget.attrs['autofocus'] = True
        return super(GetUserForm, self).as_p()

74 75
    def clean(self):
        cleaned_data = super(GetUserForm, self).clean()
Skia's avatar
Skia committed
76
        cus = None
77
        if cleaned_data['code'] != "":
Skia's avatar
Skia committed
78
            cus = Customer.objects.filter(account_id__iexact=cleaned_data['code']).first()
79
        elif cleaned_data['id'] is not None:
Skia's avatar
Skia committed
80
            cus = Customer.objects.filter(user=cleaned_data['id']).first()
81
        if (cus is None or not cus.can_buy):
Skia's avatar
Skia committed
82
            raise forms.ValidationError(_("User not found"))
Skia's avatar
Skia committed
83 84
        cleaned_data['user_id'] = cus.user.id
        cleaned_data['user'] = cus.user
85 86
        return cleaned_data

87 88 89
class RefillForm(forms.ModelForm):
    error_css_class = 'error'
    required_css_class = 'required'
Sli's avatar
Sli committed
90
    amount = forms.FloatField(min_value=0, widget=forms.NumberInput(attrs={'class':'focus'}))
91 92 93 94
    class Meta:
        model = Refilling
        fields = ['amount', 'payment_method', 'bank']

Skia's avatar
Skia committed
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
class CounterTabsMixin(TabedViewMixin):
    def get_tabs_title(self):
        return self.object
    def get_list_of_tabs(self):
        tab_list = []
        tab_list.append({
            'url': reverse_lazy('counter:details', kwargs={'counter_id': self.object.id}),
            'slug': 'counter',
            'name': _("Counter"),
            })
        if self.object.type == "BAR":
            tab_list.append({
                'url': reverse_lazy('counter:cash_summary', kwargs={'counter_id': self.object.id}),
                'slug': 'cash_summary',
                'name': _("Cash summary"),
                })
            tab_list.append({
                'url': reverse_lazy('counter:last_ops', kwargs={'counter_id': self.object.id}),
                'slug': 'last_ops',
                'name': _("Last operations"),
                })
        return tab_list

Skia's avatar
Skia committed
118
class CounterMain(CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin):
Skia's avatar
Skia committed
119 120 121
    """
    The public (barman) view
    """
Skia's avatar
Skia committed
122
    model = Counter
Skia's avatar
Skia committed
123
    template_name = 'counter/counter_main.jinja'
Skia's avatar
Skia committed
124
    pk_url_kwarg = "counter_id"
125
    form_class = GetUserForm # Form to enter a client code and get the corresponding user id
Skia's avatar
Skia committed
126
    current_tab = "counter"
Skia's avatar
Skia committed
127

128 129 130 131 132 133 134 135
    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        if self.object.type == "BAR" and not ('counter_token' in self.request.session.keys() and
                self.request.session['counter_token'] == self.object.token): # Check the token to avoid the bar to be stolen
            return HttpResponseRedirect(reverse_lazy('counter:details', args=self.args,
                kwargs={'counter_id': self.object.id})+'?bad_location')
        return super(CounterMain, self).post(request, *args, **kwargs)

Skia's avatar
Skia committed
136
    def get_context_data(self, **kwargs):
Skia's avatar
Skia committed
137
        """
138
        We handle here the login form for the barman
Skia's avatar
Skia committed
139
        """
140 141
        if self.request.method == 'POST':
            self.object = self.get_object()
Skia's avatar
Skia committed
142
        self.object.update_activity()
Skia's avatar
Skia committed
143
        kwargs = super(CounterMain, self).get_context_data(**kwargs)
Skia's avatar
Skia committed
144
        kwargs['login_form'] = LoginForm()
Skia's avatar
Skia committed
145
        kwargs['login_form'].fields['username'].widget.attrs['autofocus'] = True
Skia's avatar
Skia committed
146 147 148
        kwargs['login_form'].cleaned_data = {} # add_error fails if there are no cleaned_data
        if "credentials" in self.request.GET:
            kwargs['login_form'].add_error(None, _("Bad credentials"))
149 150
        if "sellers" in self.request.GET:
            kwargs['login_form'].add_error(None, _("User is not barman"))
Skia's avatar
Skia committed
151
        kwargs['form'] = self.get_form()
152 153
        kwargs['form'].cleaned_data = {} # same as above
        if "bad_location" in self.request.GET:
Skia's avatar
Skia committed
154
            kwargs['form'].add_error(None, _("Bad location, someone is already logged in somewhere else"))
Skia's avatar
Skia committed
155
        if self.object.type == 'BAR':
Skia's avatar
Skia committed
156
            kwargs['barmen'] = self.object.get_barmen_list()
Skia's avatar
Skia committed
157 158
        elif self.request.user.is_authenticated():
            kwargs['barmen'] = [self.request.user]
159 160 161 162 163
        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
164 165
        return kwargs

166 167 168 169 170 171 172 173 174 175
    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)

Skia's avatar
Skia committed
176
class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
Skia's avatar
Skia committed
177 178
    """
    The click view
179 180
    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
181
    """
182
    model = Counter
Skia's avatar
Skia committed
183 184
    template_name = 'counter/counter_click.jinja'
    pk_url_kwarg = "counter_id"
Skia's avatar
Skia committed
185
    current_tab = "counter"
186

187 188
    def dispatch(self, request, *args, **kwargs):
        self.customer = get_object_or_404(Customer, user__id=self.kwargs['user_id'])
Sli's avatar
Sli committed
189
        obj = self.get_object()
190 191
        if not self.customer.can_buy:
            raise Http404
Sli's avatar
Sli committed
192 193 194 195 196 197 198
        if obj.type == "BAR":
            if not ('counter_token' in request.session.keys() and
                request.session['counter_token'] == obj.token) or len(obj.get_barmen_list())<1:
                raise PermissionDenied
        else:
            if not request.user.is_authenticated():
                raise PermissionDenied
199 200
        return super(CounterClick, self).dispatch(request, *args, **kwargs)

201
    def get(self, request, *args, **kwargs):
202
        """Simple get view"""
203 204 205
        if 'basket' not in request.session.keys(): # Init the basket session entry
            request.session['basket'] = {}
            request.session['basket_total'] = 0
Skia's avatar
Skia committed
206
        request.session['not_enough'] = False # Reset every variable
207
        request.session['too_young'] = False
208
        request.session['not_allowed'] = False
Skia's avatar
Skia committed
209
        request.session['no_age'] = False
210
        self.refill_form = None
211
        ret = super(CounterClick, self).get(request, *args, **kwargs)
Skia's avatar
Skia committed
212 213
        if ((self.object.type != "BAR" and not request.user.is_authenticated()) or
                (self.object.type == "BAR" and
Skia's avatar
Skia committed
214
                len(self.object.get_barmen_list()) < 1)): # Check that at least one barman is logged in
Skia's avatar
Skia committed
215
            ret = self.cancel(request) # Otherwise, go to main view
216
        return ret
Skia's avatar
Skia committed
217 218

    def post(self, request, *args, **kwargs):
219
        """ Handle the many possibilities of the post request """
220
        self.object = self.get_object()
221
        self.refill_form = None
Skia's avatar
Skia committed
222 223
        if ((self.object.type != "BAR" and not request.user.is_authenticated()) or
                (self.object.type == "BAR" and
Skia's avatar
Skia committed
224
                len(self.object.get_barmen_list()) < 1)): # Check that at least one barman is logged in
225
            return self.cancel(request)
226 227 228 229
        if self.object.type == "BAR" and not ('counter_token' in self.request.session.keys() and
                self.request.session['counter_token'] == self.object.token): # Also check the token to avoid the bar to be stolen
            return HttpResponseRedirect(reverse_lazy('counter:details', args=self.args,
                kwargs={'counter_id': self.object.id})+'?bad_location')
230 231
        if 'basket' not in request.session.keys():
            request.session['basket'] = {}
232
            request.session['basket_total'] = 0
Skia's avatar
Skia committed
233
        request.session['not_enough'] = False # Reset every variable
234
        request.session['too_young'] = False
235
        request.session['not_allowed'] = False
Skia's avatar
Skia committed
236
        request.session['no_age'] = False
Skia's avatar
Skia committed
237 238 239 240 241
        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
242
            self.operator = self.object.get_random_barman()
243 244 245 246 247

        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
248 249
        elif 'refill' in request.POST['action']:
            self.refill(request)
Skia's avatar
Skia committed
250 251
        elif 'code' in request.POST['action']:
            return self.parse_code(request)
252 253 254 255 256 257 258
        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
259
    def is_barman_price(self):
Skia's avatar
Skia committed
260
        if self.object.type == "BAR" and self.customer.user.id in [s.id for s in self.object.get_barmen_list()]:
261 262 263 264
            return True
        else:
            return False

265 266 267
    def get_product(self, pid):
        return Product.objects.filter(pk=int(pid)).first()

268
    def get_price(self, pid):
269
        p = self.get_product(pid)
270 271 272 273 274 275 276 277 278 279
        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']
280
        return total / 100
281

Skia's avatar
Skia committed
282 283 284 285 286 287 288
    def get_total_quantity_for_pid(self, request, pid):
        pid = str(pid)
        try:
            return request.session['basket'][pid]['qty'] + request.session['basket'][pid]['bonus_qty']
        except:
            return 0

Skia's avatar
Skia committed
289
    def add_product(self, request, q = 1, p=None):
290 291 292 293 294
        """
        Add a product to the basket
        q is the quantity passed as integer
        p is the product id, passed as an integer
        """
Skia's avatar
Skia committed
295 296
        pid = p or request.POST['product_id']
        pid = str(pid)
297
        price = self.get_price(pid)
298
        total = self.sum_basket(request)
Skia's avatar
Skia committed
299
        product = self.get_product(pid)
300
        can_buy = False
301 302 303 304 305 306
        if not product.buying_groups.exists():
            can_buy = True
        else:
            for g in product.buying_groups.all():
                if self.customer.user.is_in_group(g.name):
                    can_buy = True
307 308 309
        if not can_buy:
            request.session['not_allowed'] = True
            return False
Skia's avatar
Skia committed
310 311 312 313 314
        bq = 0 # Bonus quantity, for trays
        if product.tray: # Handle the tray to adjust the quantity q to add and the bonus quantity bq
            total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6
            bq = int((total_qty_mod_6 + q) / 6) # Integer division
            q -= bq
315
        if self.customer.amount < (total + round(q*float(price),2)): # Check for enough money
316
            request.session['not_enough'] = True
Skia's avatar
Skia committed
317
            return False
Skia's avatar
Skia committed
318 319 320
        if product.limit_age >= 18 and not self.customer.user.date_of_birth:
            request.session['no_age'] = True
            return False
Sli's avatar
Sli committed
321 322 323
        if product.limit_age >= 18 and self.customer.user.is_banned_alcohol:
            request.session['not_allowed'] = True
            return False
324 325 326
        if self.customer.user.is_banned_counter:
            request.session['not_allowed'] = True
            return False
Skia's avatar
Skia committed
327
        if self.customer.user.date_of_birth and self.customer.user.get_age() < product.limit_age: # Check if affordable
328 329
            request.session['too_young'] = True
            return False
Skia's avatar
Skia committed
330
        if pid in request.session['basket']: # Add if already in basket
331
            request.session['basket'][pid]['qty'] += q
Skia's avatar
Skia committed
332 333 334
            request.session['basket'][pid]['bonus_qty'] += bq
        else: # or create if not
            request.session['basket'][pid] = {'qty': q, 'price': int(price*100), 'bonus_qty': bq}
335
        request.session.modified = True
Skia's avatar
Skia committed
336
        return True
337 338 339

    def del_product(self, request):
        """ Delete a product from the basket """
340
        pid = str(request.POST['product_id'])
Skia's avatar
Skia committed
341
        product = self.get_product(pid)
342
        if pid in request.session['basket']:
Skia's avatar
Skia committed
343 344 345 346
            if product.tray and (self.get_total_quantity_for_pid(request, pid) % 6 == 0) and request.session['basket'][pid]['bonus_qty']:
                request.session['basket'][pid]['bonus_qty'] -= 1
            else:
                request.session['basket'][pid]['qty'] -= 1
347
            if request.session['basket'][pid]['qty'] <= 0:
348
                del request.session['basket'][pid]
349
        else:
Skia's avatar
Skia committed
350
            request.session['basket'][pid] = None
351 352
        request.session.modified = True

Skia's avatar
Skia committed
353 354 355
    def parse_code(self, request):
        """Parse the string entered by the barman"""
        string = str(request.POST['code']).upper()
Skia's avatar
Skia committed
356
        if string == _("END"):
Skia's avatar
Skia committed
357
            return self.finish(request)
Skia's avatar
Skia committed
358
        elif string == _("CAN"):
Skia's avatar
Skia committed
359 360 361 362 363 364 365 366 367 368
            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
369
            p = self.object.products.filter(code=code).first()
Skia's avatar
Skia committed
370 371 372 373 374 375
            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)

376 377
    def finish(self, request):
        """ Finish the click session, and validate the basket """
Skia's avatar
Skia committed
378 379 380 381 382 383 384 385 386 387 388
        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"))
Skia's avatar
Skia committed
389
                request.session['last_basket'].append("%d x %s" % (infos['qty']+infos['bonus_qty'], p.name))
390
                s = Selling(label=p.name, product=p, club=p.club, counter=self.object, unit_price=uprice,
Skia's avatar
Skia committed
391
                       quantity=infos['qty'], seller=self.operator, customer=self.customer)
Skia's avatar
Skia committed
392
                s.save()
Skia's avatar
Skia committed
393 394 395 396
                if infos['bonus_qty']:
                    s = Selling(label=p.name + " (Plateau)", product=p, club=p.club, counter=self.object, unit_price=0,
                           quantity=infos['bonus_qty'], seller=self.operator, customer=self.customer)
                    s.save()
Skia's avatar
Skia committed
397 398 399 400 401 402 403 404 405
            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))
406 407 408 409

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

Skia's avatar
Skia committed
413 414
    def refill(self, request):
        """Refill the customer's account"""
Sli's avatar
Sli committed
415 416 417 418 419 420 421 422 423
        if self.get_object().type == 'BAR':
            form = RefillForm(request.POST)
            if form.is_valid():
                form.instance.counter = self.object
                form.instance.operator = self.operator
                form.instance.customer = self.customer
                form.instance.save()
            else:
                self.refill_form = form
424
        else:
Sli's avatar
Sli committed
425
            raise PermissionDenied
Skia's avatar
Skia committed
426

427
    def get_context_data(self, **kwargs):
428
        """ Add customer to the context """
429 430
        kwargs = super(CounterClick, self).get_context_data(**kwargs)
        kwargs['customer'] = self.customer
431
        kwargs['basket_total'] = self.sum_basket(self.request)
432
        kwargs['refill_form'] = self.refill_form or RefillForm()
433
        kwargs['categories'] = ProductType.objects.all()
434 435
        return kwargs

Skia's avatar
Skia committed
436
class CounterLogin(RedirectView):
Skia's avatar
Skia committed
437 438 439
    """
    Handle the login of a barman

440
    Logged barmen are stored in the Permanency model
Skia's avatar
Skia committed
441
    """
Skia's avatar
Skia committed
442
    permanent = False
Skia's avatar
Skia committed
443
    def post(self, request, *args, **kwargs):
Skia's avatar
Skia committed
444 445 446
        """
        Register the logged user as barman for this counter
        """
Skia's avatar
Skia committed
447
        self.counter_id = kwargs['counter_id']
448
        self.counter = Counter.objects.filter(id=kwargs['counter_id']).first()
Skia's avatar
Skia committed
449
        form = LoginForm(request, data=request.POST)
Skia's avatar
Skia committed
450
        self.errors = []
Skia's avatar
Skia committed
451
        if form.is_valid():
452
            user = User.objects.filter(username=form.cleaned_data['username']).first()
453
            if user in self.counter.sellers.all() and not user in self.counter.get_barmen_list():
454 455
                if len(self.counter.get_barmen_list()) <= 0:
                    self.counter.gen_token()
456
                request.session['counter_token'] = self.counter.token
457
                self.counter.add_barman(user)
Skia's avatar
Skia committed
458
            else:
459
                self.errors += ["sellers"]
Skia's avatar
Skia committed
460
        else:
Skia's avatar
Skia committed
461
            self.errors += ["credentials"]
Skia's avatar
Skia committed
462 463 464
        return super(CounterLogin, self).post(request, *args, **kwargs)

    def get_redirect_url(self, *args, **kwargs):
Skia's avatar
Skia committed
465
        return reverse_lazy('counter:details', args=args, kwargs=kwargs)+"?"+'&'.join(self.errors)
Skia's avatar
Skia committed
466 467 468 469

class CounterLogout(RedirectView):
    permanent = False
    def post(self, request, *args, **kwargs):
Skia's avatar
Skia committed
470 471 472
        """
        Unregister the user from the barman
        """
473 474 475
        self.counter = Counter.objects.filter(id=kwargs['counter_id']).first()
        user = User.objects.filter(id=request.POST['user_id']).first()
        self.counter.del_barman(user)
Skia's avatar
Skia committed
476 477 478 479
        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
480

481 482
## Counter admin views

Skia's avatar
Skia committed
483
class CounterAdminTabsMixin(TabedViewMixin):
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
    tabs_title = _("Counter administration")
    list_of_tabs = [
            {
                'url': reverse_lazy('counter:admin_list'),
                'slug': 'counters',
                'name': _("Counters"),
                },
            {
                'url': reverse_lazy('counter:product_list'),
                'slug': 'products',
                'name': _("Products"),
                },
            {
                'url': reverse_lazy('counter:product_list_archived'),
                'slug': 'archive',
                'name': _("Archived products"),
                },
            {
                'url': reverse_lazy('counter:producttype_list'),
                'slug': 'product_types',
                'name': _("Product types"),
                },
Skia's avatar
Skia committed
506 507 508 509 510
            {
                'url': reverse_lazy('counter:cash_summary_list'),
                'slug': 'cash_summary',
                'name': _("Cash register summaries"),
                },
Skia's avatar
Skia committed
511 512 513 514 515
            {
                'url': reverse_lazy('counter:invoices_call'),
                'slug': 'invoices_call',
                'name': _("Invoices call"),
                },
Skia's avatar
Skia committed
516 517 518 519 520
            {
                'url': reverse_lazy('counter:eticket_list'),
                'slug': 'etickets',
                'name': _("Etickets"),
                },
521 522
            ]

Sli's avatar
Sli committed
523
class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
Skia's avatar
Skia committed
524 525 526 527 528
    """
    A list view for the admins
    """
    model = Counter
    template_name = 'counter/counter_list.jinja'
529
    current_tab = "counters"
530

531 532 533 534
class CounterEditForm(forms.ModelForm):
    class Meta:
        model = Counter
        fields = ['sellers', 'products']
535 536
    sellers = make_ajax_field(Counter, 'sellers', 'users', help_text="")
    products = make_ajax_field(Counter, 'products', 'products', help_text="")
537

Sli's avatar
Sli committed
538
class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
539 540 541 542 543 544
    """
    Edit a counter's main informations (for the counter's manager)
    """
    model = Counter
    form_class = CounterEditForm
    pk_url_kwarg = "counter_id"
545
    template_name = 'core/edit.jinja'
546
    current_tab = "counters"
547

Sli's avatar
Sli committed
548 549
    def dispatch(self, request, *args, **kwargs):
        obj = self.get_object()
Sli's avatar
Sli committed
550
        self.edit_club.append(obj.club)
Sli's avatar
Sli committed
551 552
        return super(CounterEditView, self).dispatch(request, *args, **kwargs)

553 554 555
    def get_success_url(self):
        return reverse_lazy('counter:admin', kwargs={'counter_id': self.object.id})

Sli's avatar
Sli committed
556
class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
Skia's avatar
Skia committed
557
    """
Skia's avatar
Skia committed
558
    Edit a counter's main informations (for the counter's admin)
Skia's avatar
Skia committed
559 560
    """
    model = Counter
561
    form_class = modelform_factory(Counter, fields=['name', 'club', 'type'])
Skia's avatar
Skia committed
562
    pk_url_kwarg = "counter_id"
563
    template_name = 'core/edit.jinja'
564
    current_tab = "counters"
Skia's avatar
Skia committed
565

Sli's avatar
Sli committed
566
class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
Skia's avatar
Skia committed
567
    """
Skia's avatar
Skia committed
568
    Create a counter (for the admins)
Skia's avatar
Skia committed
569 570 571 572
    """
    model = Counter
    form_class = modelform_factory(Counter, fields=['name', 'club', 'type', 'products'],
            widgets={'products':CheckboxSelectMultiple})
573
    template_name = 'core/create.jinja'
574
    current_tab = "counters"
Skia's avatar
Skia committed
575

Sli's avatar
Sli committed
576
class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
Skia's avatar
Skia committed
577
    """
Skia's avatar
Skia committed
578
    Delete a counter (for the admins)
Skia's avatar
Skia committed
579 580 581 582 583
    """
    model = Counter
    pk_url_kwarg = "counter_id"
    template_name = 'core/delete_confirm.jinja'
    success_url = reverse_lazy('counter:admin_list')
584
    current_tab = "counters"
Skia's avatar
Skia committed
585

Skia's avatar
Skia committed
586 587
# Product management

Sli's avatar
Sli committed
588
class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
Skia's avatar
Skia committed
589 590 591 592 593
    """
    A list view for the admins
    """
    model = ProductType
    template_name = 'counter/producttype_list.jinja'
594
    current_tab = "product_types"
Skia's avatar
Skia committed
595

Sli's avatar
Sli committed
596
class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
Skia's avatar
Skia committed
597 598 599 600 601 602
    """
    A create view for the admins
    """
    model = ProductType
    fields = ['name', 'description', 'icon']
    template_name = 'core/create.jinja'
603
    current_tab = "products"
Skia's avatar
Skia committed
604

Sli's avatar
Sli committed
605
class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
Skia's avatar
Skia committed
606 607 608 609 610 611 612
    """
    An edit view for the admins
    """
    model = ProductType
    template_name = 'core/edit.jinja'
    fields = ['name', 'description', 'icon']
    pk_url_kwarg = "type_id"
613
    current_tab = "products"
Skia's avatar
Skia committed
614

Sli's avatar
Sli committed
615
class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
616 617 618 619 620 621 622
    """
    A list view for the admins
    """
    model = Product
    template_name = 'counter/product_list.jinja'
    queryset = Product.objects.filter(archived=True)
    ordering = ['name']
623
    current_tab = "archive"
624

Sli's avatar
Sli committed
625
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
Skia's avatar
Skia committed
626 627 628 629 630
    """
    A list view for the admins
    """
    model = Product
    template_name = 'counter/product_list.jinja'
631
    queryset = Product.objects.filter(archived=False)
Skia's avatar
Skia committed
632
    ordering = ['name']
633
    current_tab = "products"
634

635 636 637 638
class ProductEditForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = ['name', 'description', 'product_type', 'code', 'parent_product', 'buying_groups', 'purchase_price',
639
                'selling_price', 'special_selling_price', 'icon', 'club', 'limit_age', 'tray', 'archived']
640
    parent_product = AutoCompleteSelectField('products', show_help_text=False, label=_("Parent product"), required=False)
Skia's avatar
Skia committed
641
    buying_groups = AutoCompleteSelectMultipleField('groups', show_help_text=False, help_text="", label=_("Buying groups"), required=False)
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662
    club = AutoCompleteSelectField('clubs', show_help_text=False)
    counters = AutoCompleteSelectMultipleField('counters', show_help_text=False, help_text="", label=_("Counters"), required=False)

    def __init__(self, *args, **kwargs):
        super(ProductEditForm, self).__init__(*args, **kwargs)
        if self.instance.id:
            self.fields['counters'].initial = [str(c.id) for c in self.instance.counters.all()]

    def save(self, *args, **kwargs):
        ret = super(ProductEditForm, self).save(*args, **kwargs)
        if self.fields['counters'].initial:
            for cid in self.fields['counters'].initial:
                c = Counter.objects.filter(id=int(cid)).first()
                c.products.remove(self.instance)
                c.save()
        for cid in self.cleaned_data['counters']:
            c = Counter.objects.filter(id=int(cid)).first()
            c.products.add(self.instance)
            c.save()
        return ret

Sli's avatar
Sli committed
663
class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
Skia's avatar
Skia committed
664 665 666 667
    """
    A create view for the admins
    """
    model = Product
668
    form_class = ProductEditForm
Skia's avatar
Skia committed
669
    template_name = 'core/create.jinja'
670
    current_tab = "products"
Skia's avatar
Skia committed
671

Sli's avatar
Sli committed
672
class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
Skia's avatar
Skia committed
673 674 675 676
    """
    An edit view for the admins
    """
    model = Product
677
    form_class = ProductEditForm
Skia's avatar
Skia committed
678 679
    pk_url_kwarg = "product_id"
    template_name = 'core/edit.jinja'
680
    current_tab = "products"
Skia's avatar
Skia committed
681

Skia's avatar
Skia committed
682
class RefillingDeleteView(DeleteView):
Skia's avatar
Skia committed
683 684 685 686 687 688 689
    """
    Delete a refilling (for the admins)
    """
    model = Refilling
    pk_url_kwarg = "refilling_id"
    template_name = 'core/delete_confirm.jinja'

Skia's avatar
Skia committed
690 691 692 693 694 695 696 697 698 699 700 701 702 703 704
    def dispatch(self, request, *args, **kwargs):
        """
        We have here a very particular right handling, we can't inherit from CanEditPropMixin
        """
        self.object = self.get_object()
        if (timezone.now() - self.object.date <= timedelta(minutes=settings.SITH_LAST_OPERATIONS_LIMIT) and
                'counter_token' in request.session.keys() and
                request.session['counter_token'] and # check if not null for counters that have no token set
                Counter.objects.filter(token=request.session['counter_token']).exists()):
            self.success_url = reverse('counter:details', kwargs={'counter_id': self.object.counter.id})
            return super(RefillingDeleteView, self).dispatch(request, *args, **kwargs)
        elif self.object.is_owned_by(request.user):
            self.success_url = reverse('core:user_account', kwargs={'user_id': self.object.customer.user.id})
            return super(RefillingDeleteView, self).dispatch(request, *args, **kwargs)
        raise PermissionDenied
Skia's avatar
Skia committed
705

Skia's avatar
Skia committed
706
class SellingDeleteView(DeleteView):
Skia's avatar
Skia committed
707 708 709 710 711 712 713
    """
    Delete a selling (for the admins)
    """
    model = Selling
    pk_url_kwarg = "selling_id"
    template_name = 'core/delete_confirm.jinja'

Skia's avatar
Skia committed
714 715 716 717 718 719 720 721 722 723 724 725 726 727 728
    def dispatch(self, request, *args, **kwargs):
        """
        We have here a very particular right handling, we can't inherit from CanEditPropMixin
        """
        self.object = self.get_object()
        if (timezone.now() - self.object.date <= timedelta(minutes=settings.SITH_LAST_OPERATIONS_LIMIT) and
                'counter_token' in request.session.keys() and
                request.session['counter_token'] and # check if not null for counters that have no token set
                Counter.objects.filter(token=request.session['counter_token']).exists()):
            self.success_url = reverse('counter:details', kwargs={'counter_id': self.object.counter.id})
            return super(SellingDeleteView, self).dispatch(request, *args, **kwargs)
        elif self.object.is_owned_by(request.user):
            self.success_url = reverse('core:user_account', kwargs={'user_id': self.object.customer.user.id})
            return super(SellingDeleteView, self).dispatch(request, *args, **kwargs)
        raise PermissionDenied
Skia's avatar
Skia committed
729

Skia's avatar
Skia committed
730 731 732 733 734 735
# Cash register summaries

class CashRegisterSummaryForm(forms.Form):
    """
    Provide the cash summary form
    """
Sli's avatar
Sli committed
736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755
    ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0)
    twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0)
    fifty_cents = forms.IntegerField(label=_("50 cents"), required=False, min_value=0)
    one_euro = forms.IntegerField(label=_("1 euro"), required=False, min_value=0)
    two_euros = forms.IntegerField(label=_("2 euros"), required=False, min_value=0)
    five_euros = forms.IntegerField(label=_("5 euros"), required=False, min_value=0)
    ten_euros = forms.IntegerField(label=_("10 euros"), required=False, min_value=0)
    twenty_euros = forms.IntegerField(label=_("20 euros"), required=False, min_value=0)
    fifty_euros = forms.IntegerField(label=_("50 euros"), required=False, min_value=0)
    hundred_euros = forms.IntegerField(label=_("100 euros"), required=False, min_value=0)
    check_1_value = forms.DecimalField(label=_("Check amount"), required=False, min_value=0)
    check_1_quantity = forms.IntegerField(label=_("Check quantity"), required=False, min_value=0)
    check_2_value = forms.DecimalField(label=_("Check amount"), required=False, min_value=0)
    check_2_quantity = forms.IntegerField(label=_("Check quantity"), required=False, min_value=0)
    check_3_value = forms.DecimalField(label=_("Check amount"), required=False, min_value=0)
    check_3_quantity = forms.IntegerField(label=_("Check quantity"), required=False, min_value=0)
    check_4_value = forms.DecimalField(label=_("Check amount"), required=False, min_value=0)
    check_4_quantity = forms.IntegerField(label=_("Check quantity"), required=False, min_value=0)
    check_5_value = forms.DecimalField(label=_("Check amount"), required=False, min_value=0)
    check_5_quantity = forms.IntegerField(label=_("Check quantity"), required=False, min_value=0)
Skia's avatar
Skia committed
756 757 758
    comment = forms.CharField(label=_("Comment"), required=False)
    emptied = forms.BooleanField(label=_("Emptied"), required=False)

759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789
    def __init__(self, *args, **kwargs):
        instance = kwargs.pop('instance', None)
        super(CashRegisterSummaryForm, self).__init__(*args, **kwargs)
        if instance:
            self.fields['ten_cents'].initial = instance.ten_cents.quantity if instance.ten_cents else 0
            self.fields['twenty_cents'].initial = instance.twenty_cents.quantity if instance.twenty_cents else 0
            self.fields['fifty_cents'].initial = instance.fifty_cents.quantity if instance.fifty_cents else 0
            self.fields['one_euro'].initial = instance.one_euro.quantity if instance.one_euro else 0
            self.fields['two_euros'].initial = instance.two_euros.quantity if instance.two_euros else 0
            self.fields['five_euros'].initial = instance.five_euros.quantity if instance.five_euros else 0
            self.fields['ten_euros'].initial = instance.ten_euros.quantity if instance.ten_euros else 0
            self.fields['twenty_euros'].initial = instance.twenty_euros.quantity if instance.twenty_euros else 0
            self.fields['fifty_euros'].initial = instance.fifty_euros.quantity if instance.fifty_euros else 0
            self.fields['hundred_euros'].initial = instance.hundred_euros.quantity if instance.hundred_euros else 0
            self.fields['check_1_quantity'].initial = instance.check_1.quantity if instance.check_1 else 0
            self.fields['check_2_quantity'].initial = instance.check_2.quantity if instance.check_2 else 0
            self.fields['check_3_quantity'].initial = instance.check_3.quantity if instance.check_3 else 0
            self.fields['check_4_quantity'].initial = instance.check_4.quantity if instance.check_4 else 0
            self.fields['check_5_quantity'].initial = instance.check_5.quantity if instance.check_5 else 0
            self.fields['check_1_value'].initial = instance.check_1.value if instance.check_1 else 0
            self.fields['check_2_value'].initial = instance.check_2.value if instance.check_2 else 0
            self.fields['check_3_value'].initial = instance.check_3.value if instance.check_3 else 0
            self.fields['check_4_value'].initial = instance.check_4.value if instance.check_4 else 0
            self.fields['check_5_value'].initial = instance.check_5.value if instance.check_5 else 0
            self.fields['comment'].initial = instance.comment
            self.fields['emptied'].initial = instance.emptied
            self.instance = instance
        else:
            self.instance = None

    def save(self, counter=None):
Skia's avatar
Skia committed
790
        cd = self.cleaned_data
791
        summary = self.instance or CashRegisterSummary(
Skia's avatar
Skia committed
792 793 794
                counter=counter,
                user=counter.get_random_barman(),
                )
795 796
        summary.comment = cd['comment']
        summary.emptied = cd['emptied']
Skia's avatar
Skia committed
797
        summary.save()
798
        summary.items.all().delete()
Skia's avatar
Skia committed
799 800 801 802 803 804 805 806 807 808 809 810
        # Cash
        if cd['ten_cents']: CashRegisterSummaryItem(cash_summary=summary, value=0.1, quantity=cd['ten_cents']).save()
        if cd['twenty_cents']: CashRegisterSummaryItem(cash_summary=summary, value=0.2, quantity=cd['twenty_cents']).save()
        if cd['fifty_cents']: CashRegisterSummaryItem(cash_summary=summary, value=0.5, quantity=cd['fifty_cents']).save()
        if cd['one_euro']: CashRegisterSummaryItem(cash_summary=summary, value=1, quantity=cd['one_euro']).save()
        if cd['two_euros']: CashRegisterSummaryItem(cash_summary=summary, value=2, quantity=cd['two_euros']).save()
        if cd['five_euros']: CashRegisterSummaryItem(cash_summary=summary, value=5, quantity=cd['five_euros']).save()
        if cd['ten_euros']: CashRegisterSummaryItem(cash_summary=summary, value=10, quantity=cd['ten_euros']).save()
        if cd['twenty_euros']: CashRegisterSummaryItem(cash_summary=summary, value=20, quantity=cd['twenty_euros']).save()
        if cd['fifty_euros']: CashRegisterSummaryItem(cash_summary=summary, value=50, quantity=cd['fifty_euros']).save()
        if cd['hundred_euros']: CashRegisterSummaryItem(cash_summary=summary, value=100, quantity=cd['hundred_euros']).save()
        # Checks
811 812 813 814 815 816 817 818 819 820
        if cd['check_1_quantity']: CashRegisterSummaryItem(cash_summary=summary, value=cd['check_1_value'],
                quantity=cd['check_1_quantity'], check=True).save()
        if cd['check_2_quantity']: CashRegisterSummaryItem(cash_summary=summary, value=cd['check_2_value'],
                quantity=cd['check_2_quantity'], check=True).save()
        if cd['check_3_quantity']: CashRegisterSummaryItem(cash_summary=summary, value=cd['check_3_value'],
                quantity=cd['check_3_quantity'], check=True).save()
        if cd['check_4_quantity']: CashRegisterSummaryItem(cash_summary=summary, value=cd['check_4_value'],
                quantity=cd['check_4_quantity'], check=True).save()
        if cd['check_5_quantity']: CashRegisterSummaryItem(cash_summary=summary, value=cd['check_5_value'],
                quantity=cd['check_5_quantity'], check=True).save()
Skia's avatar
Skia committed
821 822 823
        if summary.items.count() < 1:
            summary.delete()

Skia's avatar
Skia committed
824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847
class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
    """
    Provide the last operations to allow barmen to delete them
    """
    model = Counter
    pk_url_kwarg = "counter_id"
    template_name = 'counter/last_ops.jinja'
    current_tab = "last_ops"

    def dispatch(self, request, *args, **kwargs):
        """
        We have here again a very particular right handling
        """
        self.object = self.get_object()
        if (self.object.get_barmen_list() and 'counter_token' in request.session.keys() and
            request.session['counter_token'] and # check if not null for counters that have no token set
            Counter.objects.filter(token=request.session['counter_token']).exists()):
            return super(CounterLastOperationsView, self).dispatch(request, *args, **kwargs)
        return HttpResponseRedirect(reverse('counter:details', kwargs={'counter_id': self.object.id})+'?bad_location')

    def get_context_data(self, **kwargs):
        """Add form to the context """
        kwargs = super(CounterLastOperationsView, self).get_context_data(**kwargs)
        threshold = timezone.now() - timedelta(minutes=settings.SITH_LAST_OPERATIONS_LIMIT)
Skia's avatar
Skia committed
848 849
        kwargs['last_refillings'] = self.object.refillings.filter(date__gte=threshold).order_by('-id')[:20]
        kwargs['last_sellings'] = self.object.sellings.filter(date__gte=threshold).order_by('-id')[:20]
Skia's avatar
Skia committed
850 851 852
        return kwargs

class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
Skia's avatar
Skia committed
853 854 855 856 857 858
    """
    Provide the cash summary form
    """
    model = Counter
    pk_url_kwarg = "counter_id"
    template_name = 'counter/cash_register_summary.jinja'
Skia's avatar
Skia committed
859 860 861 862 863 864 865 866 867 868 869 870
    current_tab = "cash_summary"

    def dispatch(self, request, *args, **kwargs):
        """
        We have here again a very particular right handling
        """
        self.object = self.get_object()
        if (self.object.get_barmen_list() and 'counter_token' in request.session.keys() and
            request.session['counter_token'] and # check if not null for counters that have no token set
            Counter.objects.filter(token=request.session['counter_token']).exists()):
            return super(CounterCashSummaryView, self).dispatch(request, *args, **kwargs)
        return HttpResponseRedirect(reverse('counter:details', kwargs={'counter_id': self.object.id})+'?bad_location')
Skia's avatar
Skia committed
871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892

    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        self.form = CashRegisterSummaryForm()
        return super(CounterCashSummaryView, self).get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        self.form = CashRegisterSummaryForm(request.POST)
        if self.form.is_valid():
            self.form.save(self.object)
            return HttpResponseRedirect(self.get_success_url())
        return super(CounterCashSummaryView, self).get(request, *args, **kwargs)

    def get_success_url(self):
        return reverse_lazy('counter:details', kwargs={'counter_id': self.object.id})

    def get_context_data(self, **kwargs):
        """ Add form to the context """
        kwargs = super(CounterCashSummaryView, self).get_context_data(**kwargs)
        kwargs['form'] = self.form
        return kwargs
Skia's avatar
Skia committed
893 894 895 896 897 898 899 900 901

class CounterActivityView(DetailView):
    """
    Show the bar activity
    """
    model = Counter
    pk_url_kwarg = "counter_id"
    template_name = 'counter/activity.jinja'

Sli's avatar
Sli committed
902
class CounterStatView(DetailView, CounterAdminMixin):
Skia's avatar
Skia committed
903 904 905 906 907 908 909 910 911 912 913 914
    """
    Show the bar stats
    """
    model = Counter
    pk_url_kwarg = "counter_id"
    template_name = 'counter/stats.jinja'

    def get_context_data(self, **kwargs):
        """ Add stats to the context """
        from django.db.models import Sum, Case, When, F, DecimalField
        kwargs = super(CounterStatView, self).get_context_data(**kwargs)
        kwargs['Customer'] = Customer
Skia's avatar
Skia committed
915
        kwargs['User'] = User
Skia's avatar
Skia committed
916 917 918 919 920 921 922 923 924 925 926 927 928 929
        semester_start = Subscription.compute_start(d=date.today(), duration=3)
        kwargs['total_sellings'] = Selling.objects.filter(date__gte=semester_start,
                counter=self.object).aggregate(total_sellings=Sum(F('quantity')*F('unit_price'),
                    output_field=CurrencyField()))['total_sellings']
        kwargs['top'] = Selling.objects.values('customer__user').annotate(
                selling_sum=Sum(
                    Case(When(counter=self.object,
                            date__gte=semester_start,
                            unit_price__gt=0,
                        then=F('unit_price')*F('quantity')),
                        output_field=CurrencyField()
                        )
                    )
                ).exclude(selling_sum=None).order_by('-selling_sum').all()[:100]
Skia's avatar
Skia committed
930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951
        kwargs['top_barman'] = Permanency.objects.values('user').annotate(
                perm_sum=Sum(
                    Case(When(counter=self.object,
                            end__gt=datetime(year=1999, month=1, day=1),
                        then=F('end')-F('start')),
                        output_field=models.DateTimeField()
                        )
                    )
                ).exclude(perm_sum=None).order_by('-perm_sum').all()[:100]
        kwargs['top_barman_semester'] = Permanency.objects.values('user').annotate(
                perm_sum=Sum(
                    Case(When(counter=self.object,
                            start__gt=semester_start,
                            end__gt=datetime(year=1999, month=1, day=1),
                        then=F('end')-F('start')),
                        output_field=models.DateTimeField()
                        )
                    )
                ).exclude(perm_sum=None).order_by('-perm_sum').all()[:100]

        kwargs['sith_date']=settings.SITH_START_DATE[0]
        kwargs['semester_start']=semester_start
Skia's avatar
Skia committed
952 953
        return kwargs

954
    def dispatch(self, request, *args, **kwargs):
Sli's avatar
Sli committed
955 956 957
        try:
            return super(CounterStatView, self).dispatch(request, *args, **kwargs)
        except:
958 959 960 961
            if (request.user.is_root
                or request.user.is_board_member
                or self.object.is_owned_by(request.user)):
                return super(CanEditMixin, self).dispatch(request, *args, **kwargs)
962 963
        raise PermissionDenied

Sli's avatar
Sli committed
964
class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin,  UpdateView):
965 966 967 968 969 970 971 972 973 974
    """Edit cash summaries"""
    model = CashRegisterSummary
    template_name = 'counter/cash_register_summary.jinja'
    context_object_name = "cashsummary"
    pk_url_kwarg = "cashsummary_id"
    form_class = CashRegisterSummaryForm
    current_tab = "cash_summary"

    def get_success_url(self):
        return reverse('counter:cash_summary_list')
Skia's avatar
Skia committed
975

976 977 978 979
class CashSummaryFormBase(forms.Form):
    begin_date = forms.DateTimeField(['%Y-%m-%d %H:%M:%S'], label=_("Begin date"), required=False, widget=SelectDateTime)
    end_date = forms.DateTimeField(['%Y-%m-%d %H:%M:%S'], label=_("End date"), required=False, widget=SelectDateTime)

Sli's avatar
Sli committed
980
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
Skia's avatar
Skia committed
981 982 983 984 985
    """Display a list of cash summaries"""
    model = CashRegisterSummary
    template_name = 'counter/cash_summary_list.jinja'
    context_object_name = "cashsummary_list"
    current_tab = "cash_summary"
Sli's avatar
Sli committed
986
    queryset = CashRegisterSummary.objects.all().order_by('-date')
Sli's avatar
Sli committed
987
    paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
Skia's avatar
Skia committed
988 989 990 991

    def get_context_data(self, **kwargs):
        """ Add sums to the context """
        kwargs = super(CashSummaryListView, self).get_context_data(**kwargs)
992 993
        form = CashSummaryFormBase(self.request.GET)
        kwargs['form'] = form
Skia's avatar
Skia committed
994 995 996
        kwargs['summaries_sums'] = {}
        kwargs['refilling_sums'] = {}
        for c in Counter.objects.filter(type="BAR").all():
997 998 999 1000 1001
            refillings = Refilling.objects.filter(counter=c)
            cashredistersummaries = CashRegisterSummary.objects.filter(counter=c)
            if form.is_valid() and form.cleaned_data['begin_date']:
                refillings = refillings.filter(date__gte=form.cleaned_data['begin_date'])
                cashredistersummaries = cashredistersummaries.filter(date__gte=form.cleaned_data['begin_date'])
Skia's avatar
Skia committed
1002
            else:
1003 1004
                last_summary = CashRegisterSummary.objects.filter(counter=c, emptied=True).order_by('-date').first()
                if last_summary:
Skia's avatar
Skia committed
1005 1006
                    refillings = refillings.filter(date__gt=last_summary.date)
                    cashredistersummaries = cashredistersummaries.filter(date__gt=last_summary.date)
1007 1008 1009 1010 1011 1012 1013 1014
                else:
                    refillings = refillings.filter(date__gte=datetime(year=1994, month=5, day=17, tzinfo=pytz.UTC)) # My birth date should be old enough
                    cashredistersummaries = cashredistersummaries.filter(date__gte=datetime(year=1994, month=5, day=17, tzinfo=pytz.UTC))
            if form.is_valid() and form.cleaned_data['end_date']:
                refillings = refillings.filter(date__lte=form.cleaned_data['end_date'])
                cashredistersummaries = cashredistersummaries.filter(date__lte=form.cleaned_data['end_date'])
            kwargs['summaries_sums'][c.name] = sum([s.get_total() for s in cashredistersummaries.all()])
            kwargs['refilling_sums'][c.name] = sum([s.amount for s in refillings.all()])
Skia's avatar
Skia committed
1015
        return kwargs
1016

Sli's avatar
Sli committed
1017
class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
Skia's avatar
Skia committed
1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030
    template_name = 'counter/invoices_call.jinja'
    current_tab = 'invoices_call'

    def get_context_data(self, **kwargs):
        """ Add sums to the context """
        kwargs = super(InvoiceCallView, self).get_context_data(**kwargs)
        kwargs['months'] = Selling.objects.datetimes('date', 'month', order='DESC')
        start_date = None
        end_date = None
        try:
            start_date = datetime.strptime(self.request.GET['month'], '%Y-%m')
        except:
            start_date = datetime(year=timezone.now().year, month=(timezone.now().month+10)%12+1, day=1)
Skia's avatar
Skia committed
1031
        start_date = start_date.replace(tzinfo=pytz.UTC)
Skia's avatar
Skia committed
1032 1033
        end_date = (start_date + timedelta(days=32)).replace(day=1, hour=0, minute=0, microsecond=0)
        from django.db.models import Sum, Case, When, F, DecimalField
Sli's avatar
Sli committed
1034 1035 1036 1037 1038 1039 1040 1041
        sum_cb  = 0
        for r in Refilling.objects.filter(payment_method='CARD', is_validated=True,
                                          date__gte=start_date, date__lte=end_date):
            sum_cb +=r.amount
        for s in Selling.objects.filter(payment_method='CARD', is_validated=True,
                                         date__gte=start_date, date__lte=end_date):
            sum_cb +=s.quantity*s.unit_price
        kwargs['sum_cb'] = sum_cb
Skia's avatar
Skia committed
1042
        kwargs['start_date'] = start_date
Skia's avatar
Skia committed
1043 1044 1045 1046 1047 1048 1049 1050 1051
        kwargs['sums'] = Selling.objects.values('club__name').annotate(selling_sum=Sum(
            Case(When(date__gte=start_date,
                date__lt=end_date,
                then=F('unit_price')*F('quantity')),
                output_field=CurrencyField()
                )
            )).exclude(selling_sum=None).order_by('-selling_sum')
        return kwargs

Sli's avatar
Sli committed
1052
class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
Skia's avatar
Skia committed
1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069
    """
    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)

Sli's avatar
Sli committed
1070
class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
Skia's avatar
Skia committed
1071 1072 1073 1074 1075 1076 1077 1078
    """
    Create an eticket
    """
    model = Eticket
    template_name = 'core/create.jinja'
    form_class = EticketForm
    current_tab = "etickets"

Sli's avatar
Sli committed
1079
class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
Skia's avatar
Skia committed
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105
    """
    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
Sli's avatar
Sli committed
1106
        code = "%s %s %s %s" % (self.object.customer.user.id, self.object.product.id, self.object.id, self.object.quantity)
Skia's avatar
Skia committed
1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136
        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)
Skia's avatar
Skia committed
1137
            p.drawCentredString(10.5 * cm, 22.6 * cm, eticket.event_date.strftime("%d %b %Y")) # FIXME with a locale
Skia's avatar
Skia committed
1138
        p.setFont("Helvetica-Bold", 14)
1139
        p.drawCentredString(10.5 * cm, 15 * cm, "%s : %d %s" % (user.get_display_name(), self.object.quantity, str(_("people(s)"))))
Skia's avatar
Skia committed
1140 1141 1142 1143 1144 1145 1146 1147 1148 1149
        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)

Krophil's avatar
Krophil committed
1150 1151 1152 1153 1154 1155 1156
        partners = ImageReader("core/static/core/img/partners.png")
        width, height = partners.getSize()
        size = max(width, height)
        width = width * 2 / 3
        height = height * 2 / 3
        p.drawImage(partners, 0 * cm, 0 * cm, width, height)

Skia's avatar
Skia committed