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