Completed
Push — master ( a38950...f61274 )
by
unknown
01:14
created

get_item_from_barcode()   B

Complexity

Conditions 5

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
dl 0
loc 23
rs 8.2508
c 1
b 0
f 0
1
from pyramid.events import subscriber
2
from pyramid.events import BeforeRender
3
from pyramid.httpexceptions import HTTPFound
4
from pyramid.renderers import render
5
from pyramid.renderers import render_to_response
6
from pyramid.response import Response
7
from pyramid.view import view_config, forbidden_view_config
8
9
from pyramid.i18n import TranslationStringFactory, get_localizer
10
_ = TranslationStringFactory('betty')
11
12
from sqlalchemy.sql import func
13
from sqlalchemy.exc import DBAPIError
14
from sqlalchemy.orm.exc import NoResultFound
15
16
from .models import *
17
from .models.model import *
18
from .models import user as __user
19
from .models.user import User
20
from .models.item import Item
21
from .models.box import Box
22
from .models.transaction import Transaction, BTCDeposit, PurchaseLineItem
23
from .models.account import Account, VirtualAccount, CashAccount
24
from .models.event import Event
25
from .models.announcement import Announcement
26
from .models.btcdeposit import BtcPendingDeposit
27
from .models.pool import Pool
28
from .models.tag import Tag
29
from .models.ephemeron import Ephemeron
30
31
from .utility import user_password_reset
32
from .utility import send_email
33
34
from pyramid.security import Allow, Everyone, remember, forget
35
36
import chezbetty.datalayer as datalayer
37
import transaction
38
39
import math
40
41
# Custom exception
42
class DepositException(Exception):
43
    pass
44
45
46
# Check for a valid user by UMID.
47
#
48
# Input: form encoded umid=11223344
49
#
50
# Note: will not create new user if this user does not exist.
51
@view_config(route_name='terminal_umid_check',
52
             request_method='POST',
53
             renderer='json',
54
             permission='service')
55
def terminal_umid_check(request):
56
    try:
57
        User.from_umid(request.POST['umid'])
58
        return {}
59
    except:
60
        return {'error': get_localizer(request).translate(
61
                    _('mcard-keypad-error',
62
                      default='First time using Betty? You need to swipe your M-Card the first time you log in.'))}
63
64
65
## Main terminal page with purchase/cash deposit.
66
@view_config(route_name='terminal',
67
             renderer='templates/terminal/terminal.jinja2',
68
             permission='service')
69
def terminal(request):
70
    try:
71
        if len(request.matchdict['umid']) != 8:
72
            raise __user.InvalidUserException()
73
74
        with transaction.manager:
75
            user = User.from_umid(request.matchdict['umid'], create_if_never_seen=True)
76
        user = DBSession.merge(user)
77
        if not user.enabled:
78
            request.session.flash(get_localizer(request).translate(_(
79
                'user-not-enabled',
80
                default='User is not enabled. Please contact ${email}.',
81
                mapping={'email':request.registry.settings['chezbetty.email']},
82
                )), 'error')
83
            return HTTPFound(location=request.route_url('index'))
84
85
        # Handle re-instating archived user
86
        if user.archived:
87
            if user.archived_balance != 0:
88
                datalayer.adjust_user_balance(user,
89
                                              user.archived_balance,
90
                                              'Reinstated archived user.',
91
                                              request.user)
92
            user.balance = user.archived_balance
93
            user.archived = False
94
95
        # NOTE TODO (added on 2016/05/14): The "name" field in this temp
96
        # table needs to be terminal specific. That is, if there are multiple
97
        # terminals, items and money shouldn't be able to move between them.
98
99
        # If cash was added before a user was logged in, credit that now
100
        deposit = None
101
        in_flight_deposit = Ephemeron.from_name('deposit')
102
        if in_flight_deposit:
103
            amount = Decimal(in_flight_deposit.value)
104
            deposit = datalayer.deposit(user, user, amount)
105
            DBSession.delete(in_flight_deposit)
106
107
        # For Demo mode:
108
        items = DBSession.query(Item)\
109
                         .filter(Item.enabled == True)\
110
                         .order_by(Item.name)\
111
                         .limit(6).all()
112
113
        # Determine initial wall-of-shame fee (if applicable)
114
        purchase_fee_percent = Decimal(0)
115
        if user.balance <= Decimal('-5.0'):
116
            purchase_fee_percent = 5 + (math.floor((user.balance+5) / -5) * 5)
117
118
        # Figure out if any pools can be used to pay for this purchase
119
        purchase_pools = []
120
        for pool in Pool.all_by_owner(user, True):
121
            if pool.balance > (pool.credit_limit * -1):
122
                purchase_pools.append(pool)
123
124
        for pu in user.pools:
125
            if pu.pool.enabled and pu.pool.balance > (pu.pool.credit_limit * -1):
126
                purchase_pools.append(pu.pool)
127
128
        # Get the list of tags that have items without barcodes in them
129
        tags_with_nobarcode_items = Tag.get_tags_with_nobarcode_items();
130
131
        # Add any pre-scanned items
132
        cart_html = ''
133
        cart = Ephemeron.from_name('cart')
134
        if cart:
135
            barcodes = cart.value.split(',')
136
            barcode_dict = {}
137
            # Group barcodes if an item was scanned more than once
138
            for barcode in barcodes:
139
                if barcode in barcode_dict:
140
                    barcode_dict[barcode] += 1
141
                else:
142
                    barcode_dict[barcode] = 1
143
144
            for barcode, quantity in barcode_dict.items():
145
                item = get_item_from_barcode(barcode)
146
147
                if type(item) is Item:
148
                    cart_html += render('templates/terminal/purchase_item_row.jinja2', {'item': item,
149
                                                                                        'quantity': quantity})
150
151
            # Remove temporary cart now that we've shown it to the user
152
            DBSession.delete(cart)
153
154
        return {'user': user,
155
                'items': items,
156
                'purchase_pools': purchase_pools,
157
                'purchase_fee_percent': purchase_fee_percent,
158
                'tags_with_nobarcode_items': tags_with_nobarcode_items,
159
                'existing_items': cart_html,
160
                'deposit': deposit}
161
162
    except __user.InvalidUserException as e:
163
        request.session.flash(get_localizer(request).translate(_(
164
            'mcard-error',
165
            default='Failed to read M-Card. Please try swiping again.',
166
            )), 'error')
167
        return HTTPFound(location=request.route_url('index'))
168
169
170
## Get all items without barcodes in a tag
171
@view_config(route_name='terminal_purchase_tag',
172
             renderer='json',
173
             permission='service')
174
def terminal_purchase_tag(request):
175
    try:
176
        tag_id = int(request.matchdict['tag_id'])
177
        tag = Tag.from_id(tag_id)
178
    except:
179
        if request.matchdict['tag_id'] == 'other':
180
            tag = {'name': 'other',
181
                   'nobarcode_items': Item.get_nobarcode_notag_items()}
182
        else:
183
            return {'error': 'Unable to parse TAG ID'}
184
185
    item_array = render('templates/terminal/purchase_nobarcode_items.jinja2',
186
                        {'tag': tag})
187
188
    return {'items_html': item_array}
189
190
191
## Add a cash deposit.
192
@view_config(route_name='terminal_deposit',
193
             request_method='POST',
194
             renderer='json',
195
             permission='service')
196
def terminal_deposit(request):
197
    try:
198
        if request.POST['umid'] == '':
199
            # User was not logged in when deposit was made. We store
200
            # this deposit temporarily and give it to the next user who
201
            # logs in.
202
            user = None
203
        else:
204
            user = User.from_umid(request.POST['umid'])
205
        amount = Decimal(request.POST['amount'])
206
        method = request.POST['method']
207
208
        # Can't deposit a negative amount
209
        if amount <= 0.0:
210
            raise DepositException('Deposit amount must be greater than $0.00')
211
212
        # Now check the deposit method. We trust anything that comes from the
213
        # bill acceptor, but double check a manual deposit
214
        if method == 'manual':
215
            # Check if the deposit amount is too great.
216
            if amount > 2.0:
217
                # Anything above $2 is blocked
218
                raise DepositException('Deposit amount of ${:,.2f} exceeds the limit'.format(amount))
219
220
        elif method == 'acceptor':
221
            # Any amount is OK
222
            pass
223
224
        else:
225
            raise DepositException('"{}" is an unknown deposit type'.format(method))
226
227
        # At this point the deposit can go through
228
        ret = {}
229
230
        if user:
231
            deposit = datalayer.deposit(user, user, amount)
232
            ret['type'] = 'user'
233
            ret['amount'] = float(deposit['amount'])
234
            ret['event_id'] = deposit['event'].id
235
            ret['user_balance'] = float(user.balance)
236
237
        else:
238
            # No one was logged in. Need to save this temporarily
239
            total_stored = datalayer.temporary_deposit(amount);
240
            ret['type'] = 'temporary'
241
            ret['new_amount'] = float(amount)
242
            ret['total_amount'] = float(total_stored)
243
244
        return ret
245
246
    except __user.InvalidUserException as e:
247
        request.session.flash('Invalid user error. Please try again.', 'error')
248
        return {'error': 'Error finding user.'}
249
250
    except ValueError as e:
251
        return {'error': 'Error understanding deposit amount.'}
252
253
    except DepositException as e:
254
        return {'error': str(e)}
255
256
    except Exception as e:
257
        return {'error': str(e)}
258
259
260
# ## Delete a just completed transaction.
261
# @view_config(route_name='terminal_deposit_delete',
262
#              request_method='POST',
263
#              renderer='json',
264
#              permission='service')
265
# def terminal_deposit_delete(request):
266
#     try:
267
#         user = User.from_umid(request.POST['umid'])
268
#         old_event = Event.from_id(request.POST['old_event_id'])
269
270
#         if old_event.type != 'deposit' or \
271
#            old_event.transactions[0].type != 'cashdeposit' or \
272
#            (old_event.transactions[0].to_account_virt_id != user.id and \
273
#             old_event.user_id != user.id):
274
#            # Something went wrong, can't undo this deposit
275
#            raise DepositException('Cannot undo that deposit')
276
277
#         # Now undo old deposit
278
#         datalayer.undo_event(old_event, user)
279
280
#         purchase_pools = []
281
#         for pool in Pool.all_by_owner(user, True):
282
#             if pool.balance > (pool.credit_limit * -1):
283
#                 purchase_pools.append({'id': pool.id, 'balance': float(pool.balance)})
284
285
#         for pu in user.pools:
286
#             if pu.pool.enabled and pu.pool.balance > (pu.pool.credit_limit * -1):
287
#                 purchase_pools.append({'id': pu.pool.id, 'balance': float(pu.pool.balance)})
288
289
#         return {'user_balance': float(user.balance),
290
#                 'pools': purchase_pools}
291
292
#     except __user.InvalidUserException as e:
293
#         return {'error': 'Invalid user error. Please try again.'}
294
295
#     except DepositException as e:
296
#         return {'error': str(e)}
297
298
#     except Exception as e:
299
#         if request.debug: raise(e)
300
#         return {'error': 'Error.'}
301
302
303
def get_item_from_barcode(barcode):
304
    try:
305
        item = Item.from_barcode(barcode)
306
    except:
307
        # Could not find the item. Check to see if the user scanned a box
308
        # instead. This could lead to two cases: a) the box only has 1 item in it
309
        # in which case we just add that item to the cart. This likely occurred
310
        # because the individual items do not have barcodes so we just use
311
        # the box. b) The box has multiple items in it in which case we throw
312
        # an error for now.
313
        try:
314
            box = Box.from_barcode(barcode)
315
            if box.subitem_number == 1:
316
                item = box.items[0].item
317
            else:
318
                return 'Cannot add that entire box to your order. Please scan an individual item.'
319
        except:
320
            return 'Could not find that item.'
321
322
    if not item.enabled:
323
        return 'That product is not currently for sale.'
324
325
    return item
326
327
328
## Add an item to a shopping cart.
329
@view_config(route_name='terminal_item_barcode',
330
             renderer='json',
331
             permission='service')
332
def terminal_item_barcode(request):
333
    item = get_item_from_barcode(request.matchdict['barcode'])
334
335
    if type(item) is str:
336
         return {'error': item}
337
338
    item_html = render('templates/terminal/purchase_item_row.jinja2', {'item': item})
339
    return {'id':item.id,
340
            'item_row_html': item_html}
341
342
343
## Add item to saved list for when a user logs in (when we can add it to
344
## the cart)
345
@view_config(route_name='terminal_saveitem_barcode',
346
             renderer='json',
347
             permission='service')
348
def terminal_saveitem_barcode(request):
349
    try:
350
        cart_barcodes = Ephemeron.add_list('cart', request.matchdict['barcode'])
351
        return {'barcodes': cart_barcodes}
352
    except Exception as e:
353
        if request.debug: raise(e)
354
        return {'error': 'Error.'}
355
356
357
## Add an item to a shopping cart.
358
@view_config(route_name='terminal_item_id',
359
             renderer='json',
360
             permission='service')
361
def terminal_item_id(request):
362
    try:
363
        item = Item.from_id(request.matchdict['item_id'])
364
    except:
365
        return {'error': 'Could not find that item.'}
366
367
    if not item.enabled:
368
        return {'error': 'That product is not currently for sale.'}
369
370
    item_html = render('templates/terminal/purchase_item_row.jinja2', {'item': item})
371
    return {'id': item.id,
372
            'item_row_html': item_html}
373
374
375
## Make a purchase from the terminal.
376
@view_config(route_name='terminal_purchase',
377
             request_method='POST',
378
             renderer='json',
379
             permission='service')
380
def terminal_purchase(request):
381
    try:
382
        user = User.from_umid(request.POST['umid'])
383
384
        ignored_keys = ['umid', 'pool_id']
385
386
        # Bundle all purchase items
387
        items = {}
388
        for item_id,quantity in request.POST.items():
389
            if item_id in ignored_keys:
390
                continue
391
            item = Item.from_id(int(item_id))
392
            items[item] = int(quantity)
393
394
        # What should pay for this?
395
        # Note: should do a bunch of checking to make sure all of this
396
        # is kosher. But given our locked down single terminal, we're just
397
        # going to skip all of that.
398
        if 'pool_id' in request.POST:
399
            pool = Pool.from_id(int(request.POST['pool_id']))
400
            purchase = datalayer.purchase(user, pool, items)
401
        else:
402
            purchase = datalayer.purchase(user, user, items)
403
404
        # Create a order complete view
405
        order = {'total': purchase.amount,
406
                 'discount': purchase.discount,
407
                 'items': []}
408
        for subtrans in purchase.subtransactions:
409
            item = {}
410
            item['name'] = subtrans.item.name
411
            item['quantity'] = subtrans.quantity
412
            item['price'] = subtrans.item.price
413
            item['total_price'] = subtrans.amount
414
            order['items'].append(item)
415
416
        if purchase.fr_account_virt_id == user.id:
417
            account_type = 'user'
418
            pool = None
419
        else:
420
            account_type = 'pool'
421
            pool = Pool.from_id(purchase.fr_account_virt_id)
422
423
        summary = render('templates/terminal/purchase_complete.jinja2',
424
            {'user': user,
425
             'event': purchase.event,
426
             'order': order,
427
             'transaction': purchase,
428
             'account_type': account_type,
429
             'pool': pool})
430
431
        # Return the committed transaction ID
432
        return {'order_table': summary,
433
                'user_balance': float(user.balance)}
434
435
    except __user.InvalidUserException as e:
436
        return {'error': get_localizer(request).translate(_('invalid-user-error',
437
                           default='Invalid user error. Please try again.'))
438
               }
439
440
    except ValueError as e:
441
        return {'error': 'Unable to parse Item ID or quantity'}
442
443
    except NoResultFound as e:
444
        # Could not find an item
445
        return {'error': 'Unable to identify an item.'}
446
447
448
## Delete a just completed purchase.
449
@view_config(route_name='terminal_purchase_delete',
450
             request_method='POST',
451
             renderer='json',
452
             permission='service')
453
def terminal_purchase_delete(request):
454
    try:
455
        user = User.from_umid(request.POST['umid'])
456
        old_event = Event.from_id(request.POST['old_event_id'])
457
458
        if old_event.type != 'purchase' or \
459
           old_event.transactions[0].type != 'purchase' or \
460
           (old_event.transactions[0].fr_account_virt_id != user.id and \
461
            old_event.user_id != user.id):
462
           # Something went wrong, can't undo this purchase
463
           raise DepositException('Cannot undo that purchase')
464
465
        # Now undo old deposit
466
        datalayer.undo_event(old_event, user)
467
468
        return {'user_balance': float(user.balance)}
469
470
    except __user.InvalidUserException as e:
471
        return {'error': 'Invalid user error. Please try again.'}
472
473
    except DepositException as e:
474
        return {'error': str(e)}
475
476
477