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

deposit()   D

Complexity

Conditions 8

Size

Total Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

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