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