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