Completed
Push — master ( d2aebd...b0ae7e )
by
unknown
01:01
created

purchase()   F

Complexity

Conditions 9

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 9
dl 0
loc 36
rs 3
1
import functools
2
3
from .models.model import *
4
from .models import event
5
from .models import transaction
6
from .models import account
7
from .models.pool import Pool
8
from .models.user import User
9
from .models import request
10
from .models import receipt
11
from .models.item import Item
12
from .models.box import Box
13
from .models import box_item
14
from .models import item_vendor
15
from .models import box_vendor
16
17
from .utility import notify_pool_out_of_credit
18
from .utility import notify_new_top_wall_of_shame
19
20
import math
21
22
23
def top_debtor_wrapper(fn):
24
    '''Wrapper function for transactions that watches for a new top debtor.
25
26
    Should wrap any function that creates a purchase or deposit transaction.
27
    Can't put this inside the Transaction class b/c the add/flush operations are
28
    at a higher level.'''
29
    @functools.wraps(fn)
30
    def wrapper(*args, **kwargs):
31
        # Record top debtor before new transaction
32
        old_top_debtor = DBSession.query(User).order_by(User.balance).limit(1).one()
33
34
        # Execute transaction. Txn function should call add() and flush()
35
        ret = fn(*args, **kwargs)
36
37
        # Check whether the top debtor has changed
38
        new_top_debtor = DBSession.query(User).order_by(User.balance).limit(1).one()
39
        print(old_top_debtor, new_top_debtor)
40
        if new_top_debtor != old_top_debtor:
41
            notify_new_top_wall_of_shame(new_top_debtor)
42
43
        return ret
44
45
    return wrapper
46
47
48
def can_undo_event(e):
49
    if e.type != 'deposit' and e.type != 'purchase' and e.type != 'restock' \
50
       and e.type != 'inventory' and e.type != 'emptycashbox' \
51
       and e.type != 'donation' and e.type != 'withdrawal':
52
        return False
53
    if e.deleted:
54
        return False
55
    return True
56
57
58
# Call this to remove an event from chez betty. Only works with cash deposits
59
def undo_event(e, user):
60
    assert(can_undo_event(e))
61
62
    line_items = {}
63
64
    for t in e.transactions:
65
66
        if t.to_account_virt:
67
            t.to_account_virt.balance -= t.amount
68
        if t.fr_account_virt:
69
            t.fr_account_virt.balance += t.amount
70
        if t.to_account_cash:
71
            t.to_account_cash.balance -= t.amount
72
        if t.fr_account_cash:
73
            t.fr_account_cash.balance += t.amount
74
75
        if t.type == 'purchase':
76
            # Re-add the stock to the items that were purchased
77
            for s in t.subtransactions:
78
                line_items[s.item_id] = s.quantity
79
                Item.from_id(s.item_id).in_stock += s.quantity
80
81
        elif t.type == 'restock':
82
            # Include the global cost so we can repopulate the box on the
83
            # restock page.
84
            line_items[0] = '{}'.format(t.amount_restock_cost)
85
86
            # Add all of the boxes and items to the return list
87
            # Also remove the stock this restock added to each item
88
            for i,s in zip(range(len(t.subtransactions)), t.subtransactions):
89
                if s.type == 'restocklineitem':
90
                    item = Item.from_id(s.item_id)
91
                    line_items[i+1] = '{},{},{},{},{},{},{}'.format(
92
                        'item', s.item_id, s.quantity, s.wholesale,
93
                        s.coupon_amount, s.sales_tax, s.bottle_deposit)
94
                    item.in_stock -= s.quantity
95
                elif s.type == 'restocklinebox':
96
                    line_items[i+1] = '{},{},{},{},{},{},{}'.format(
97
                        'box', s.box_id, s.quantity, s.wholesale,
98
                        s.coupon_amount, s.sales_tax, s.bottle_deposit)
99
                    for ss in s.subsubtransactions:
100
                        item = Item.from_id(ss.item_id)
101
                        item.in_stock -= ss.quantity
102
103
104
105
        elif t.type == 'inventory':
106
            # Change the stock of all the items by reversing the inventory count
107
            for s in t.subtransactions:
108
                quantity_diff = s.quantity - s.quantity_counted
109
                s.item.in_stock += quantity_diff
110
                line_items[s.item_id] = s.quantity_counted
111
112
        # Don't need anything for emptycashbox. On those transactions no
113
        # other tables are changed.
114
115
116
    # Just need to delete the event. All transactions will understand they
117
    # were deleted as well.
118
    e.delete(user)
119
120
    return line_items
121
122
def can_delete_item(item):
123
    if len(item.boxes) == 0 and\
124
       len(item.vendors) == 0 and\
125
       len(item.subtransactions) == 0 and\
126
       len(item.subsubtransactions) == 0:
127
       return True
128
    return False
129
130
def delete_item(item):
131
    boxitems = DBSession.query(box_item.BoxItem).filter(box_item.BoxItem.item_id==item.id).all()
132
    for bi in boxitems:
133
        DBSession.delete(bi)
134
    itemvendors = DBSession.query(item_vendor.ItemVendor).filter(item_vendor.ItemVendor.item_id==item.id).all()
135
    for iv in itemvendors:
136
        DBSession.delete(iv)
137
    DBSession.delete(item)
138
139
def can_delete_box(box):
140
    if len(box.items) == 0 and\
141
       len(box.vendors) == 0 and\
142
       len(box.subtransactions) == 0:
143
       return True
144
    return False
145
146
def delete_box(box):
147
    boxitems = DBSession.query(box_item.BoxItem).filter(box_item.BoxItem.box_id==box.id).all()
148
    for bi in boxitems:
149
        DBSession.delete(bi)
150
    boxvendors = DBSession.query(box_vendor.BoxVendor).filter(box_vendor.BoxVendor.box_id==box.id).all()
151
    for bv in boxvendors:
152
        DBSession.delete(bv)
153
    DBSession.delete(box)
154
155
156
# Call this to make a new item request
157
def new_request(user, request_text):
158
    r = request.Request(user, request_text)
159
    DBSession.add(r)
160
    DBSession.flush()
161
    return r
162
163
164
# Call this to let a user purchase items
165
@top_debtor_wrapper
166
def purchase(user, account, items):
167
    assert(hasattr(user, "id"))
168
    assert(len(items) > 0)
169
170
    # TODO: Parameterize
171
    discount = None
172
    if user.balance > 20.0:
173
        discount = Decimal('0.05')
174
    elif user.balance <= -5.0:
175
        discount = round(Decimal('-0.01')*(5 + math.floor((user.balance+5) / -5)), 2)
176
177
    e = event.Purchase(user)
178
    DBSession.add(e)
179
    DBSession.flush()
180
    t = transaction.Purchase(e, account, discount)
181
    DBSession.add(t)
182
    DBSession.flush()
183
    amount = Decimal(0)
184
    for item, quantity in items.items():
185
        item.in_stock -= quantity
186
        line_amount = Decimal(item.price * quantity)
187
        pli = transaction.PurchaseLineItem(t, line_amount, item, quantity,
188
                                           item.price, item.wholesale)
189
        DBSession.add(pli)
190
        amount += line_amount
191
    if discount:
192
        amount = round(amount - (amount * discount), 2)
193
    t.update_amount(amount)
194
195
    if isinstance(account, Pool):
196
        if account.balance < (account.credit_limit * -1):
197
            owner = User.from_id(account.owner)
198
            notify_pool_out_of_credit(owner, account)
199
200
    return t
201
202
203
# Call this when a user puts money in the dropbox and needs to deposit it
204
# to their account
205 View Code Duplication
@top_debtor_wrapper
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
206
def deposit(user, account, amount):
207
    assert(amount > 0.0)
208
    assert(hasattr(user, "id"))
209
210
    prev = user.balance
211
    e = event.Deposit(user)
212
    DBSession.add(e)
213
    DBSession.flush()
214
    t = transaction.CashDeposit(e, account, amount)
215
    DBSession.add(t)
216
    return dict(prev=prev,
217
                new=user.balance,
218
                amount=amount,
219
                transaction=t,
220
                event=e)
221
222
223
# Call this when a credit card transaction deposits money into an account
224 View Code Duplication
@top_debtor_wrapper
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
225
def cc_deposit(user, account, amount, txn_id, last4):
226
    assert(amount > 0.0)
227
    assert(hasattr(user, "id"))
228
229
    prev = user.balance
230
    e = event.Deposit(user)
231
    DBSession.add(e)
232
    DBSession.flush()
233
    t = transaction.CCDeposit(e, account, amount, txn_id, last4)
234
    DBSession.add(t)
235
    return dict(prev=prev,
236
                new=user.balance,
237
                amount=amount,
238
                transaction=t,
239
                event=e)
240
241
242
# Call this to deposit bitcoins to the user account
243
@top_debtor_wrapper
244
def bitcoin_deposit(user, amount, btc_transaction, address, amount_btc):
245
    assert(amount > 0.0)
246
    assert(hasattr(user, "id"))
247
248
    prev = user.balance
249
    e = event.Deposit(user)
250
    DBSession.add(e)
251
    DBSession.flush()
252
    t = transaction.BTCDeposit(e, user, amount, btc_transaction, address, amount_btc)
253
    DBSession.add(t)
254
    return dict(prev=prev,
255
                new=user.balance,
256
                amount=amount,
257
                transaction=t)
258
259
260
# Call this to adjust a user's balance
261
@top_debtor_wrapper
262
def adjust_user_balance(user, adjustment, notes, admin):
263
    e = event.Adjustment(admin, notes)
264
    DBSession.add(e)
265
    DBSession.flush()
266
    t = transaction.Adjustment(e, user, adjustment)
267
    DBSession.add(t)
268
    return t
269
270
271
# Call this when an admin restocks chezbetty
272
def restock(items, global_cost, admin, timestamp=None):
273
    e = event.Restock(admin, timestamp)
274
    DBSession.add(e)
275
    DBSession.flush()
276
    t = transaction.Restock(e, Decimal(global_cost))
277
    DBSession.add(t)
278
    DBSession.flush()
279
    # Start with the global cost when calculating the total amount
280
    amount = Decimal(global_cost)
281
282
    # Add all of the items as subtransactions
283
    for thing, quantity, total, wholesale, coupon, salestax, btldeposit in items:
284
        if type(thing) is Item:
285
            item = thing
286
            # Add the stock to the item
287
            item.in_stock += quantity
288
            # Make sure the item is enabled (now that we have some in stock)
289
            item.enabled = True
290
            # Create a subtransaction to track that this item was added
291
            rli = transaction.RestockLineItem(t, total, item, quantity, wholesale, coupon, salestax, btldeposit)
292
            DBSession.add(rli)
293
            #amount += Decimal(total)
294
295
        elif type(thing) is Box:
296
            box = thing
297
298
            # Create a subtransaction to record that the box was restocked
299
            rlb = transaction.RestockLineBox(t, total, box, quantity, wholesale, coupon, salestax, btldeposit)
300
            DBSession.add(rlb)
301
            DBSession.flush()
302
303
            # Iterate all the subitems and update the stock
304
            for itembox in box.items:
305
                subitem = itembox.item
306
                subquantity = itembox.quantity * quantity
307
                subitem.enabled = True
308
                subitem.in_stock += subquantity
309
310
                rlbi = transaction.RestockLineBoxItem(rlb, subitem, subquantity)
311
                DBSession.add(rlbi)
312
313
        amount += Decimal(total)
314
315
    t.update_amount(amount)
316
    return e
317
318
319
# Call this when a user runs inventory
320
def reconcile_items(items, admin):
321
    e = event.Inventory(admin)
322
    DBSession.add(e)
323
    DBSession.flush()
324
    t = transaction.Inventory(e)
325
    DBSession.add(t)
326
    DBSession.flush()
327
    total_amount_missing = Decimal(0)
328
    for item, quantity in items.items():
329
        # Record the restock line item even if the number hasn't changed.
330
        # This lets us track when we have counted items.
331
        quantity_missing = item.in_stock - quantity
332
        line_amount = quantity_missing * item.wholesale
333
        ili = transaction.InventoryLineItem(t, line_amount, item, item.in_stock,
334
                                quantity, item.wholesale)
335
        DBSession.add(ili)
336
        total_amount_missing += ili.amount
337
        item.in_stock = quantity
338
    t.update_amount(total_amount_missing)
339
    DBSession.add(t)
340
    DBSession.flush()
341
    return t
342
343
344
# Call this when the cash box gets emptied
345 View Code Duplication
def reconcile_safe(amount, admin):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
346
    assert(amount>=0)
347
348
    e = event.EmptySafe(admin)
349
    DBSession.add(e)
350
    DBSession.flush()
351
352
    safe_c = account.get_cash_account("safe")
353
    expected_amount = safe_c.balance
354
    amount_missing = expected_amount - amount
355
356
    if amount_missing != 0.0:
357
        # If the amount in the safe doesn't match what we expected there to
358
        # be, we need to adjust the amount in the cash box be transferring
359
        # to or from a null account
360
361
        if amount_missing > 0:
362
            # We got less in the box than we expected
363
            # Move money from the safe account to null with transaction type
364
            # "lost"
365
            t1 = transaction.Lost(e, account.get_cash_account("safe"), amount_missing)
366
            DBSession.add(t1)
367
368
        else:
369
            # We got more in the box than expected! Use a found transaction
370
            # to reconcile the difference
371
            t1 = transaction.Found(e, account.get_cash_account("safe"), abs(amount_missing))
372
            DBSession.add(t1)
373
374
375
    # Now move all the money from the safe to chezbetty
376
    t2 = transaction.EmptySafe(e, amount)
377
    DBSession.add(t2)
378
    return e
379
380
381
# Call this to move all of the money from the cash box to the safe.
382
# We don't actually count the amount, so we do no reconciling here, but it
383
# means that money isn't sitting in the store.
384
def cashbox_to_safe(admin):
385
    e = event.EmptyCashBox(admin)
386
    DBSession.add(e)
387
    DBSession.flush()
388
389
    t = transaction.EmptyCashBox(e)
390
    DBSession.add(t)
391
    return e
392
393
394
# Call this to move money from the safe to the bank.
395
def safe_to_bank(amount, admin):
396
    assert(amount>=0)
397
398
    e = event.EmptySafe(admin)
399
    DBSession.add(e)
400
    DBSession.flush()
401
402
    t = transaction.EmptySafe(e, amount)
403
    DBSession.add(t)
404
    return e
405
406
407
# Call this when bitcoins are converted to USD
408 View Code Duplication
def reconcile_bitcoins(amount, admin, expected_amount=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
409
    assert(amount>0)
410
411
    e = event.EmptyBitcoin(admin)
412
    DBSession.add(e)
413
    DBSession.flush()
414
415
    btcbox_c = account.get_cash_account("btcbox")
416
    if expected_amount == None:
417
        expected_amount = btcbox_c.balance
418
    amount_missing = expected_amount - amount
419
420
    if amount_missing != 0.0:
421
        # Value of bitcoins fluctated and we didn't make as much as we expected
422
423
        if amount_missing > 0:
424
            # We got less in bitcoins than we expected
425
            # Move money from the btcbox account to null with transaction type
426
            # "lost"
427
            t1 = transaction.Lost(e, account.get_cash_account("btcbox"), amount_missing)
428
            DBSession.add(t1)
429
430
        else:
431
            # We got more in bitcoins than expected! Use a found transaction
432
            # to reconcile the difference
433
            t1 = transaction.Found(e, account.get_cash_account("btcbox"), abs(amount_missing))
434
            DBSession.add(t1)
435
436
437
    # Now move all the money from the bitcoin box to chezbetty
438
    t2 = transaction.EmptyBitcoin(e, amount)
439
    DBSession.add(t2)
440
    return expected_amount
441
442
443
# Call this to make a miscellaneous adjustment to the chezbetty account
444
def reconcile_misc(amount, notes, admin):
445
    assert(amount != 0.0)
446
447
    e = event.Reconcile(admin, notes)
448
    DBSession.add(e)
449
    DBSession.flush()
450
451
    if amount < 0.0:
452
        t = transaction.Lost(e, account.get_cash_account("chezbetty"), abs(amount))
453
    else:
454
        t = transaction.Found(e, account.get_cash_account("chezbetty"), amount)
455
    DBSession.add(t)
456
    return t
457
458
459
# Call this to make a cash donation to Chez Betty
460
def add_donation(amount, notes, admin, timestamp=None):
461
    e = event.Donation(admin, notes, timestamp)
462
    DBSession.add(e)
463
    DBSession.flush()
464
    t = transaction.Donation(e, amount)
465
    DBSession.add(t)
466
    return t
467
468
469
# Call this to withdraw cash funds from Chez Betty into another account
470
def add_withdrawal(amount, notes, admin, timestamp=None):
471
    e = event.Withdrawal(admin, notes, timestamp)
472
    DBSession.add(e)
473
    DBSession.flush()
474
    t = transaction.Withdrawal(e, amount)
475
    DBSession.add(t)
476
    return t
477
478
def upload_receipt(event, admin, rfile):
479
    r = receipt.Receipt(event, admin, rfile)
480
    DBSession.add(r)
481
    DBSession.flush()
482
    return r
483