Completed
Push — master ( d0a2b0...997812 )
by Pat
57s
created

chezbetty.purchase()   D

Complexity

Conditions 8

Size

Total Lines 32

Duplication

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