Completed
Push — master ( c56c12...56a8bb )
by
unknown
59s
created

chezbetty.cashbox_to_bank()   A

Complexity

Conditions 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 2
dl 0
loc 9
rs 9.6666

1 Method

Rating   Name   Duplication   Size   Complexity  
A chezbetty.cashbox_to_safe() 0 8 1
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
@top_debtor_wrapper
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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
@top_debtor_wrapper
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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
def reconcile_safe(amount, admin):
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
def reconcile_bitcoins(amount, admin, expected_amount=None):
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