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 |
|
|
|
|
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 |
|
|
|
|
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
|
|
|
|
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.