Completed
Push — master ( 1fa280...2b11ba )
by
unknown
01:21
created

__relevant_cash_deposits()   D

Complexity

Conditions 2

Size

Total Lines 19

Duplication

Lines 1
Ratio 5.26 %
Metric Value
cc 2
dl 1
loc 19
rs 4.8292
1
from .model import *
2
from . import account
3
from . import event
4
from . import item
5
from . import box
6
from . import user
7
from chezbetty import utility
8
9
import arrow
10
from pyramid.threadlocal import get_current_registry
11
12
13
class Transaction(Base):
14
    __tablename__ = 'transactions'
15
16
    id                 = Column(Integer, primary_key=True, nullable=False)
17
18
    event_id           = Column(Integer, ForeignKey("events.id"))
19
20
    to_account_virt_id = Column(Integer, ForeignKey("accounts.id"))
21
    fr_account_virt_id = Column(Integer, ForeignKey("accounts.id"))
22
    to_account_cash_id = Column(Integer, ForeignKey("accounts.id"))
23
    fr_account_cash_id = Column(Integer, ForeignKey("accounts.id"))
24
    amount             = Column(Numeric, nullable=False)
25
26
                                        # Virtual Transaction Meaning     # Cash Transaction Meaning  # Notes required?
27
    type = Column(Enum("purchase",      # user_account -> chezbetty.        None
28
                       "deposit",
29
                       "cashdeposit",   # null         -> user_account.     null      -> cashbox.
30
                       "ccdeposit",     # null         -> user_account.     null      -> chezbetty
31
                       "btcdeposit",    # null         -> user_account      null      -> btcbox
32
                       "adjustment",    # chezbetty   <-> user              None                            Yes
33
                       "restock",       # chezbetty    -> null              chezbetty -> null
34
                       "inventory",     # chezbetty   <-> null              None
35
                       "emptycashbox",  # None                              cashbox   -> safe
36
                       "emptysafe",     # None                              safe      -> chezbetty
37
                       "emptybitcoin",  # None                              btcbox    -> chezbetty
38
                       "lost",          # None                              chezbetty/cashbox/btcbox -> null       Yes
39
                       "found",         # None                              null      -> chezbetty/cashbox/btcbox  Yes
40
                       "donation",      # null         -> chezbetty         null      -> chezbetty          Yes
41
                       "withdrawal",    # chezbetty    -> null              chezbetty -> null               Yes
42
                       name="transaction_type"), nullable=False)
43
    __mapper_args__ = {'polymorphic_on': type}
44
45
46
    to_account_virt = relationship(account.Account,
47
        foreign_keys=[to_account_virt_id,],
48
        backref="transactions_to_virt"
49
    )
50
    fr_account_virt = relationship(account.Account,
51
        foreign_keys=[fr_account_virt_id,],
52
        backref="transactions_from_virt"
53
    )
54
55
    to_account_cash = relationship(account.Account,
56
        foreign_keys=[to_account_cash_id,],
57
        backref="transactions_to_cash"
58
    )
59
    fr_account_cash = relationship(account.Account,
60
        foreign_keys=[fr_account_cash_id,],
61
        backref="transactions_from_cash"
62
    )
63
    event = relationship(event.Event,
64
        foreign_keys=[event_id,],
65
        backref="transactions"
66
    )
67
68
69
    def __init__(self, event, fr_acct_virt, to_acct_virt, fr_acct_cash, to_acct_cash, amount):
70
        self.to_account_virt_id = to_acct_virt.id if to_acct_virt else None
71
        self.fr_account_virt_id = fr_acct_virt.id if fr_acct_virt else None
72
        self.to_account_cash_id = to_acct_cash.id if to_acct_cash else None
73
        self.fr_account_cash_id = fr_acct_cash.id if fr_acct_cash else None
74
75
        self.to_acct_virt = to_acct_virt
76
        self.fr_acct_virt = fr_acct_virt
77
        self.to_acct_cash = to_acct_cash
78
        self.fr_acct_cash = fr_acct_cash
79
80
        self.event_id = event.id
81
        self.amount = amount
82
83
        # Update the balances of the accounts we are moving money between
84
        if to_acct_virt:
85
            to_acct_virt.balance += self.amount
86
        if fr_acct_virt:
87
            fr_acct_virt.balance -= self.amount
88
89
        if to_acct_cash:
90
            to_acct_cash.balance += self.amount
91
        if fr_acct_cash:
92
            fr_acct_cash.balance -= self.amount
93
94
    def update_amount(self, amount):
95
        # Remove the balance we added before (upon init or last update_amount)
96
        if self.to_acct_virt:
97
            self.to_acct_virt.balance -= self.amount
98
        if self.fr_acct_virt:
99
            self.fr_acct_virt.balance += self.amount
100
        if self.to_acct_cash:
101
            self.to_acct_cash.balance -= self.amount
102
        if self.fr_acct_cash:
103
            self.fr_acct_cash.balance += self.amount
104
105
        # Save the amount so we can subtract it later if needed
106
        self.amount = amount
107
108
        # Apply the new amount
109
        if self.to_acct_virt:
110
            self.to_acct_virt.balance += self.amount
111
        if self.fr_acct_virt:
112
            self.fr_acct_virt.balance -= self.amount
113
        if self.to_acct_cash:
114
            self.to_acct_cash.balance += self.amount
115
        if self.fr_acct_cash:
116
            self.fr_acct_cash.balance -= self.amount
117
118
    @classmethod
119
    def from_id(cls, id):
120
        return DBSession.query(cls)\
121
                        .filter(cls.id == id).one()
122
123
    @classmethod
124
    def get_balance(cls, trans_type, account_obj):
125
        return DBSession.query(coalesce(func.sum(cls.amount), 0).label("balance"))\
126
                     .join(event.Event)\
127
                     .filter(or_(cls.fr_account_cash_id==account_obj.id,
128
                                 cls.to_account_cash_id==account_obj.id,
129
                                 cls.fr_account_virt_id==account_obj.id,
130
                                 cls.to_account_virt_id==account_obj.id))\
131
                     .filter(cls.type==trans_type)\
132
                     .filter(event.Event.deleted==False).one()
133
134
    @classmethod
135
    def count(cls, trans_type=None):
136
        if not trans_type:
137
            return DBSession.query(func.count(cls.id).label('c'))\
138
                            .join(event.Event)\
139
                            .filter(event.Event.deleted==False).one().c
140
        else:
141
            return DBSession.query(func.count(cls.id).label('c'))\
142
                            .join(event.Event)\
143
                            .filter(cls.type==trans_type)\
144
                            .filter(event.Event.deleted==False).one().c
145
146 View Code Duplication
    @classmethod
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
147
    def total(cls, start=None, end=None):
148
        r = DBSession.query(func.sum(cls.amount).label('a'))\
149
                        .join(event.Event)\
150
                        .filter(event.Event.deleted==False)
151
152
        if start:
153
            r = r.filter(event.Event.timestamp>=start)
154
        if end:
155
            r = r.filter(event.Event.timestamp<end)
156
157
        return r.one().a or Decimal(0.0)
158
159
    # Get the total amount of discounts people have received for keeping
160
    # money in their account
161 View Code Duplication
    @classmethod
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
162
    def discounts(cls, start=None, end=None):
163
        r = DBSession.query(func.sum((cls.amount / (1-cls.discount)) - cls.amount).label('d'))\
164
                        .join(event.Event)\
165
                        .filter(cls.discount > 0)\
166
                        .filter(event.Event.deleted==False)
167
168
        if start:
169
            r = r.filter(event.Event.timestamp>=start)
170
        if end:
171
            r = r.filter(event.Event.timestamp<end)
172
173
        return r.one().d or Decimal(0.0)
174
175
    # Get the total amount of fees people have paid for being in debt
176 View Code Duplication
    @classmethod
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
177
    def fees(cls, start=None, end=None):
178
        r = DBSession.query(func.sum((cls.amount / (1-cls.discount)) - cls.amount).label('f'))\
179
                        .join(event.Event)\
180
                        .filter(cls.discount < 0)\
181
                        .filter(event.Event.deleted==False)
182
183
        if start:
184
            r = r.filter(event.Event.timestamp>=start)
185
        if end:
186
            r = r.filter(event.Event.timestamp<end)
187
188
        return r.one().f or Decimal(0.0)
189
190
    # Returns an array of tuples where the first item is a millisecond timestamp,
191
    # the next is the total amount of debt, and the next is the total amount
192
    # of stored money for users.
193 View Code Duplication
    @classmethod
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
194
    def get_balance_total_daily(cls):
195
        rows = DBSession.query(cls.amount,
196
                               cls.type,
197
                               cls.to_account_virt_id,
198
                               cls.fr_account_virt_id,
199
                               event.Event.timestamp)\
200
                        .join(event.Event)\
201
                        .filter(event.Event.deleted==False)\
202
                        .filter(or_(
203
                                  cls.type=='purchase',
204
                                  cls.type=='cashdeposit',
205
                                  cls.type=='ccdeposit',
206
                                  cls.type=='btcdeposit',
207
                                  cls.type=='adjustment'
208
                                ))\
209
                        .order_by(event.Event.timestamp)\
210
                        .all()
211
        return utility.timeseries_balance_total_daily(rows)
212
213
214 View Code Duplication
    @classmethod
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
215
    def get_transactions_over_time_for_user(cls, user):
216
        return DBSession.query(cls.amount,
217
                               cls.type,
218
                               cls.to_account_virt_id,
219
                               cls.fr_account_virt_id,
220
                               event.Event.timestamp)\
221
                        .join(event.Event)\
222
                        .filter(event.Event.deleted==False)\
223
                        .filter(or_(
224
                                  cls.type=='purchase',
225
                                  cls.type=='cashdeposit',
226
                                  cls.type=='ccdeposit',
227
                                  cls.type=='btcdeposit',
228
                                  cls.type=='adjustment'
229
                                ))\
230
                        .filter(or_(
231
                            cls.to_account_virt_id == user.id,
232
                            cls.fr_account_virt_id == user.id,
233
                            ))\
234
                        .order_by(event.Event.timestamp)\
235
                        .all()
236
237
238
    @classmethod
239
    def get_balances_over_time_for_user(cls, user):
240
        rows = cls.get_transactions_over_time_for_user(user)
241
        # We can re-use the global balance calculation code because the query
242
        # filtered it down to only this user, only now the "global" total
243
        # positive values (r[2]) and total debt (r[1]) are just this user's
244
        # balance, so we pull out the right column at each point in time.
245
        rows = utility.timeseries_balance_total_daily(rows)
246
        rows = [(r[0],r[2]/100 if r[1]==0 else -r[1]/100) for r in rows]
247
        return rows
248
249
    @classmethod
250
    def get_days_in_debt_for_user(cls, user):
251
        rows = cls.get_transactions_over_time_for_user(user)
252
        days = utility.get_days_on_shame(user, rows)
253
        return days
254
255
256
def __get_transactions_query(self):
257
    return object_session(self).query(Transaction)\
258
            .join(event.Event)\
259
            .filter(or_(
260
                      or_(
261
                        Transaction.to_account_virt_id == self.id,
262
                        Transaction.fr_account_virt_id == self.id,
263
                        Transaction.to_account_cash_id == self.id,
264
                        Transaction.fr_account_cash_id == self.id),
265
                      and_(
266
                        or_(event.Event.type == "purchase",
267
                            event.Event.type == "deposit"),
268
                        event.Event.user_id == self.id)))\
269
            .filter(event.Event.deleted==False)\
270
            .order_by(desc(event.Event.timestamp))\
271
272
@limitable_all
273
def __get_transactions(self):
274
    return __get_transactions_query(self)
275
276
@property
277
def __transactions(self):
278
    return __get_transactions(self)
279
280
account.Account.get_transactions_query = __get_transactions_query
281
account.Account.get_transactions = __get_transactions
282
account.Account.transactions = __transactions
283
284
# This is in a stupid place due to circular input problems
285 View Code Duplication
@limitable_all
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
286
def __get_events(self):
287
    return object_session(self).query(event.Event)\
288
            .join(Transaction)\
289
            .filter(or_(
290
                      or_(
291
                        Transaction.to_account_virt_id == self.id,
292
                        Transaction.fr_account_virt_id == self.id,
293
                        Transaction.to_account_cash_id == self.id,
294
                        Transaction.fr_account_cash_id == self.id),
295
                      and_(
296
                        or_(event.Event.type == "purchase",
297
                            event.Event.type == "deposit"),
298
                        event.Event.user_id == self.id)))\
299
            .filter(event.Event.deleted==False)\
300
            .order_by(desc(event.Event.timestamp))
301
302
@property
303
def __events(self):
304
    return __get_events(self).all()
305
306
account.Account.get_events = __get_events
307
account.Account.events = __events
308
309
# This is in a stupid place due to circular input problems
310
@property
311
def __total_deposit_amount(self):
312
    return object_session(self).query(func.sum(Transaction.amount).label("total"))\
313
            .join(event.Event)\
314
            .filter(and_(
315
                        Transaction.to_account_virt_id == self.id,
316
                        or_(Transaction.type == 'cashdeposit',
317
                            Transaction.type == 'ccdeposit',
318
                            Transaction.type == 'btcdeposit')))\
319
            .filter(event.Event.deleted==False).one().total or Decimal(0.0)
320
account.Account.total_deposits = __total_deposit_amount
321
322
# This is in a stupid place due to circular input problems
323
@property
324
def __total_purchase_amount(self):
325
    return object_session(self).query(func.sum(Transaction.amount).label("total"))\
326
            .join(event.Event)\
327
            .filter(and_(
328
                        Transaction.fr_account_virt_id == self.id,
329
                        Transaction.type == 'purchase'))\
330
            .filter(event.Event.deleted==False).one().total or Decimal(0.0)
331
account.Account.total_purchases = __total_purchase_amount
332
333
# This is in a stupid place due to circular input problems
334
@classmethod
335
@limitable_all
336
def __get_cash_events(cls):
337
    return DBSession.query(event.Event)\
338
            .join(Transaction)\
339
            .filter(or_(
340
                      Transaction.to_account_cash_id == account.get_cash_account("chezbetty").id,
341
                      Transaction.fr_account_cash_id == account.get_cash_account("chezbetty").id))\
342
            .filter(event.Event.deleted==False)\
343
            .order_by(desc(event.Event.timestamp))
344
event.Event.get_cash_events = __get_cash_events
345
346
# This is in a stupid place due to circular input problems
347
@classmethod
348
@limitable_all
349
def __get_restock_events(cls):
350
    return DBSession.query(event.Event)\
351
            .join(Transaction)\
352
            .filter(Transaction.type == 'restock')\
353
            .filter(event.Event.deleted==False)\
354
            .order_by(desc(event.Event.timestamp))
355
event.Event.get_restock_events = __get_restock_events
356
357
# This is in a stupid place due to circular input problems
358
@classmethod
359
@limitable_all
360
def __get_emptycash_events(cls):
361
    return DBSession.query(event.Event)\
362
            .join(Transaction)\
363
            .filter(or_(
364
                      Transaction.type == 'emptycashbox',
365
                      Transaction.type == 'emptysafe',
366
                      Transaction.type == 'emptybitcoin'))\
367
            .filter(event.Event.deleted==False)\
368
            .order_by(desc(event.Event.timestamp))
369
event.Event.get_emptycash_events = __get_emptycash_events
370
371
# This is in a stupid place due to circular input problems
372
@classmethod
373
@limitable_all
374
def __get_deposit_events(cls):
375
    return DBSession.query(event.Event)\
376
            .join(Transaction)\
377
            .filter(or_(
378
                      Transaction.type == 'cashdeposit',
379
                      Transaction.type == 'ccdeposit',
380
                      Transaction.type == 'btcdeposit'))\
381
            .filter(event.Event.deleted==False)\
382
            .order_by(desc(event.Event.timestamp))
383
event.Event.get_deposit_events = __get_deposit_events
384
385
# This is in a stupid place due to circular input problems
386
@classmethod
387
@limitable_all
388
def __get_donation_events(cls):
389
    return DBSession.query(event.Event)\
390
            .join(Transaction)\
391
            .filter(or_(
392
                      Transaction.type == 'donation',
393
                      Transaction.type == 'withdrawal'))\
394
            .filter(event.Event.deleted==False)\
395
            .order_by(desc(event.Event.timestamp))
396
event.Event.get_donation_events = __get_donation_events
397
398
# This is in a stupid place due to circular input problems
399
@classmethod
400
def __get_deadbeats(cls):
401
    deadbeats = DBSession.query(user.User)\
402
            .filter(user.User.enabled==True)\
403
            .filter(user.User.archived==False)\
404
            .filter(user.User.balance <= -5)\
405
            .all()
406
407
    # Only get users between 0 and -5 if they have been in debt for a week or
408
    # more.
409
    iffy_users = DBSession.query(user.User)\
410
            .filter(user.User.enabled==True)\
411
            .filter(user.User.archived==False)\
412
            .filter(user.User.balance < 0)\
413
            .filter(user.User.balance > -5)\
414
            .all()
415
    for u in iffy_users:
416
        days = Transaction.get_days_in_debt_for_user(u)
417
        if days >= 7:
418
            deadbeats.append(u)
419
420
    return deadbeats
421
user.User.get_deadbeats = __get_deadbeats
422
423
# This is in a stupid place due to circular input problems
424 View Code Duplication
@property
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
425
def __days_since_last_purchase(self):
426
    last_purchase = object_session(self).query(event.Event)\
427
            .join(Transaction)\
428
            .filter(Transaction.fr_account_virt_id == self.id)\
429
            .filter(event.Event.type == 'purchase')\
430
            .filter(event.Event.deleted==False)\
431
            .order_by(desc(event.Event.timestamp)).first()
432
433
    if last_purchase:
434
        diff = arrow.now() - last_purchase.timestamp
435
        return diff.days
436
    else:
437
        return None
438
user.User.days_since_last_purchase = __days_since_last_purchase
439
440
# This is in a stupid place due to circular input problems
441
@property
442
def __lifetime_fees(self):
443
    return object_session(self).query(func.sum((Purchase.amount / (1-Purchase.discount)) - Purchase.amount).label("f"))\
444
            .join(event.Event)\
445
            .filter(Purchase.fr_account_virt_id == self.id)\
446
            .filter(Purchase.discount < 0)\
447
            .filter(event.Event.type == 'purchase')\
448
            .filter(event.Event.deleted==False).one().f or Decimal(0.0)
449
user.User.lifetime_fees = __lifetime_fees
450
451
# This is in a stupid place due to circular input problems
452
@property
453
def __lifetime_discounts(self):
454
    return object_session(self).query(func.sum((Purchase.amount / (1-Purchase.discount)) - Purchase.amount).label("f"))\
455
            .join(event.Event)\
456
            .filter(Purchase.fr_account_virt_id == self.id)\
457
            .filter(Purchase.discount > 0)\
458
            .filter(event.Event.type == 'purchase')\
459
            .filter(event.Event.deleted==False).one().f or Decimal(0.0)
460
user.User.lifetime_discounts = __lifetime_discounts
461
462
463
@property
464
def __relevant_cash_deposits(self):
465
    # Get the cashbox empty before this one
466
    previous_cb_empty = object_session(self).query(event.Event)\
467
            .filter(event.Event.type == 'emptycashbox')\
468
            .filter(event.Event.timestamp < self.timestamp)\
469
            .filter(event.Event.deleted == False)\
470
            .order_by(desc(event.Event.timestamp))\
471
            .first()
472
473
    # Now get all cash deposits between that cash box empty and this one
474
    q = object_session(self).query(event.Deposit)\
475
            .filter(event.Event.timestamp < self.timestamp)\
476
            .filter(event.Event.deleted==False)
477
478
    if previous_cb_empty:
479
        q = q.filter(event.Event.timestamp >= previous_cb_empty.timestamp)
480
481 View Code Duplication
    return q.all()
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
482
event.EmptyCashBox.relevant_cash_deposits = __relevant_cash_deposits
483
484
################################################################################
485
## Related Classes
486
################################################################################
487
488
class Purchase(Transaction):
489
    __mapper_args__ = {'polymorphic_identity': 'purchase'}
490
    discount = Column(Numeric)
491
492
    def __init__(self, event, user, discount=None):
493
        chezbetty_v = account.get_virt_account("chezbetty")
494
        Transaction.__init__(self, event, user, chezbetty_v, None, None, Decimal(0.0))
495
        self.discount = discount
496
497
498
class Deposit(Transaction):
499
    __mapper_args__ = {'polymorphic_identity': 'deposit'}
500
501
    @classmethod
502
    def deposits_by_period(cls, period, start=None, end=None):
503
        r = DBSession.query(cls.amount.label('summable'), event.Event.timestamp)\
504
                     .join(event.Event)\
505
                     .order_by(event.Event.timestamp)\
506
                     .filter(event.Event.deleted==False)
507
        if start:
508
            r = r.filter(event.Event.timestamp>=start.replace(tzinfo=None))
509
        if end:
510
            r = r.filter(event.Event.timestamp<end.replace(tzinfo=None))
511
512
        return utility.group(r.all(), period)
513
514
515
class CashDeposit(Deposit):
516
    __mapper_args__ = {'polymorphic_identity': 'cashdeposit'}
517
518
    CONTENTS_THRESHOLD = 1000
519
    REPEAT_THRESHOLD = 100
520
521
    def __init__(self, event, user, amount):
522
        cashbox_c = account.get_cash_account("cashbox")
523
        prev = cashbox_c.balance
524
        Transaction.__init__(self, event, None, user, None, cashbox_c, amount)
525
        new = cashbox_c.balance
526
527
        # It feels like the model should not have all of this application
528
        # specific logic in it. What does sending an email have to do with
529
        # representing a transaction. I think this should be moved to
530
        # datalayer.py which does handle application logic.
531
        try:
532
            if prev < CashDeposit.CONTENTS_THRESHOLD and new > CashDeposit.CONTENTS_THRESHOLD:
533
                self.send_alert_email(new)
534
            elif prev > CashDeposit.CONTENTS_THRESHOLD:
535
                pr = int((prev - CashDeposit.CONTENTS_THRESHOLD) / CashDeposit.REPEAT_THRESHOLD)
536
                nr = int((new - CashDeposit.CONTENTS_THRESHOLD) / CashDeposit.REPEAT_THRESHOLD)
537
                if pr != nr:
538
                    self.send_alert_email(new, nr)
539
        except:
540
            # Some error sending email. Let's not prevent the deposit from
541
            # going through.
542
            pass
543
544
    def send_alert_email(self, amount, repeat=0):
545
        settings = get_current_registry().settings
546
547
        SUBJECT = 'Time to empty Betty. Cash box has ${}.'.format(amount)
548
        TO = '[email protected]'
549
550
        body = """
551
        <p>Betty's cash box is getting full. Time to go to the bank.</p>
552
        <p>The cash box currently contains ${}.</p>
553
        """.format(amount)
554
        if repeat > 8:
555
            body = """
556
            <p><strong>Yo! Get your shit together! That's a lot of cash lying
557
            around!</strong></p>""" + body
558
        elif repeat > 4:
559
            body = body + """
560
            <p><strong>But seriously, you should probably go empty the cashbox
561
            like, right meow.</strong></p>"""
562
563
        if 'debugging' in settings and bool(int(settings['debugging'])):
564
            SUBJECT = '[ DEBUG_MODE ] ' + SUBJECT
565
            body = """
566
            <p><em>This message was sent from a debugging session and may be
567
            safely ignored.</em></p>""" + body
568
569
        utility.send_email(TO=TO, SUBJECT=SUBJECT, body=body)
570
571
572
class CCDeposit(Deposit):
573
    __mapper_args__ = {'polymorphic_identity': 'ccdeposit'}
574
575
    stripe_id = Column(Text)
576
    cc_last4 = Column(Text)
577
578
    def __init__(self, event, user, amount, stripe_id, last4):
579
        chezbetty_c = account.get_cash_account("chezbetty")
580
        Transaction.__init__(self, event, None, user, None, chezbetty_c, amount)
581
        self.stripe_id = stripe_id
582
        self.cc_last4 = last4
583
584
585
class BTCDeposit(Deposit):
586
    __mapper_args__ = {'polymorphic_identity': 'btcdeposit'}
587
588
    btctransaction = Column(String(64))
589
    address        = Column(String(64))
590
    amount_btc     = Column(Numeric, nullable=True)
591
592
    def __init__(self, event, user, amount, btctransaction, address, amount_btc):
593
        btcbox_c = account.get_cash_account("btcbox")
594
        Transaction.__init__(self, event, None, user, None, btcbox_c, amount)
595
        self.btctransaction = btctransaction
596
        self.address = address
597
        self.amount_btc = amount_btc
598
599
    def __getattr__(self, name):
600
        if name == 'img':
601
            return utility.string_to_qrcode(self.btctransaction)
602
        else:
603
            raise AttributeError
604
605
    @classmethod
606
    def from_address(cls, address):
607
        return DBSession.query(cls).join(event.Event)\
608
                        .filter(cls.address == address)\
609
                        .filter(event.Event.deleted == False).one()
610
611
612
class Adjustment(Transaction):
613
    __mapper_args__ = {'polymorphic_identity': 'adjustment'}
614
615
    def __init__(self, event, user, amount):
616
        chezbetty_v = account.get_virt_account("chezbetty")
617
        Transaction.__init__(self, event, chezbetty_v, user, None, None, amount)
618
619
620
class Restock(Transaction):
621
    __mapper_args__ = {'polymorphic_identity': 'restock'}
622
623
    # Additional cost that should get distributed over the entire restock
624
    amount_restock_cost = Column(Numeric, nullable=True)
625
626
    def __init__(self, event, global_cost):
627
        chezbetty_v = account.get_virt_account("chezbetty")
628
        chezbetty_c = account.get_cash_account("chezbetty")
629
        Transaction.__init__(self, event, chezbetty_v, None, chezbetty_c, None, Decimal(0.0))
630
        self.amount_restock_cost = global_cost
631
632
633
class Inventory(Transaction):
634
    __mapper_args__ = {'polymorphic_identity': 'inventory'}
635
    def __init__(self, event):
636
        chezbetty_v = account.get_virt_account("chezbetty")
637
        Transaction.__init__(self, event, chezbetty_v, None, None, None, Decimal(0.0))
638
639
640
class EmptyCashBox(Transaction):
641
    __mapper_args__ = {'polymorphic_identity': 'emptycashbox'}
642
    def __init__(self, event):
643
        cashbox_c = account.get_cash_account("cashbox")
644
        amount = cashbox_c.balance
645
        safe_c = account.get_cash_account("safe")
646
        Transaction.__init__(self, event, None, None, cashbox_c, safe_c, amount)
647
648
649
class EmptySafe(Transaction):
650
    __mapper_args__ = {'polymorphic_identity': 'emptysafe'}
651
    def __init__(self, event, amount):
652
        safe_c = account.get_cash_account("safe")
653
        chezbetty_c = account.get_cash_account("chezbetty")
654
        Transaction.__init__(self, event, None, None, safe_c, chezbetty_c, amount)
655
656
657
class EmptyBitcoin(Transaction):
658
    __mapper_args__ = {'polymorphic_identity': 'emptybitcoin'}
659
    def __init__(self, event, amount):
660
        btnbox_c = account.get_cash_account("btcbox")
661
        chezbetty_c = account.get_cash_account("chezbetty")
662
        Transaction.__init__(self, event, None, None, btnbox_c, chezbetty_c, amount)
663
664
665
class Lost(Transaction):
666
    __mapper_args__ = {'polymorphic_identity': 'lost'}
667
    def __init__(self, event, source_acct, amount):
668
        Transaction.__init__(self, event, None, None, source_acct, None, amount)
669
670
671
class Found(Transaction):
672
    __mapper_args__ = {'polymorphic_identity': 'found'}
673
    def __init__(self, event, dest_acct, amount):
674
        Transaction.__init__(self, event, None, None, None, dest_acct, amount)
675
676
677
class Donation(Transaction):
678
    __mapper_args__ = {'polymorphic_identity': 'donation'}
679
    def __init__(self, event, amount):
680
        chezbetty_v = account.get_virt_account("chezbetty")
681
        chezbetty_c = account.get_cash_account("chezbetty")
682
        Transaction.__init__(self, event, None, chezbetty_v, None, chezbetty_c, amount)
683
684
685
class Withdrawal(Transaction):
686
    __mapper_args__ = {'polymorphic_identity': 'withdrawal'}
687
    def __init__(self, event, amount):
688
        chezbetty_v = account.get_virt_account("chezbetty")
689
        chezbetty_c = account.get_cash_account("chezbetty")
690
        Transaction.__init__(self, event, chezbetty_v, None, chezbetty_c, None, amount)
691
692
693
################################################################################
694
## SUB TRANSACTIONS
695
################################################################################
696
697
class SubTransaction(Base):
698
    __tablename__   = "subtransactions"
699
700
    id              = Column(Integer, primary_key=True, nullable=False)
701
    transaction_id  = Column(Integer, ForeignKey("transactions.id"), nullable=False)
702
    amount          = Column(Numeric, nullable=False)
703
    type            = Column(Enum("purchaselineitem", "restocklineitem",
704
                                  "restocklinebox", "inventorylineitem",
705
                                  name="subtransaction_type"), nullable=False)
706
    item_id         = Column(Integer, ForeignKey("items.id"), nullable=True)
707
    quantity        = Column(Integer, nullable=False)
708
    wholesale       = Column(Numeric, nullable=False)
709
710
    # For restocks
711
    coupon_amount   = Column(Numeric, nullable=True) # Amount of discount on the item
712
    sales_tax       = Column(Boolean, nullable=True) # Whether sales tax was charged
713
    bottle_deposit  = Column(Boolean, nullable=True) # Whether there was a bottle deposit
714
715
    transaction     = relationship(Transaction, backref="subtransactions", cascade="all")
716
    item            = relationship(item.Item, backref="subtransactions")
717
718
    __mapper_args__ = {'polymorphic_on': type}
719
720
    def __init__(self, transaction, amount, item_id, quantity, wholesale):
721
        self.transaction_id = transaction.id
722
        self.amount = amount
723
        self.item_id = item_id
724
        self.quantity = quantity
725
        self.wholesale = wholesale
726
727
    def __getattr__(self, name):
728
        if name == 'deleted':
729
            return self.transaction.event.deleted
730
        else:
731
            raise AttributeError
732
733
    @classmethod
734
    @limitable_all
735
    def all_item(cls, id):
736
        return DBSession.query(cls)\
737
                        .join(Transaction)\
738
                        .join(event.Event)\
739
                        .filter(cls.item_id == id)\
740
                        .filter(event.Event.deleted==False)\
741
                        .order_by(desc(event.Event.timestamp))
742
743
    @classmethod
744
    @limitable_all
745 View Code Duplication
    def all_item_purchases(cls, id):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
746
        return DBSession.query(cls)\
747
                        .join(Transaction)\
748
                        .join(event.Event)\
749
                        .filter(cls.item_id == id)\
750
                        .filter(event.Event.deleted==False)\
751
                        .filter(event.Event.type=="purchase")\
752
                        .order_by(desc(event.Event.timestamp))
753
754
    @classmethod
755
    @limitable_all
756
    def all_item_events(cls, id):
757
        return DBSession.query(cls)\
758
                        .join(Transaction)\
759
                        .join(event.Event)\
760
                        .filter(cls.item_id == id)\
761
                        .filter(event.Event.deleted==False)\
762
                        .filter(or_(event.Event.type=="inventory", event.Event.type =="restock"))\
763
                        .order_by(desc(event.Event.timestamp))
764
765
    @classmethod
766
    @limitable_all
767
    def all(cls, trans_type=None):
768
        if not trans_type:
769 View Code Duplication
            return DBSession.query(cls)\
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
770
                            .join(Transaction)\
771
                            .join(event.Event)\
772
                            .filter(event.Event.deleted==False)\
773
                            .order_by(desc(event.Event.timestamp))
774
        else:
775
            return DBSession.query(cls)\
776
                            .join(Transaction)\
777
                            .join(event.Event)\
778
                            .filter(cls.type==trans_type)\
779
                            .filter(event.Event.deleted==False)\
780
                            .order_by(desc(event.Event.timestamp))
781
782 View Code Duplication
class PurchaseLineItem(SubTransaction):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
783
    __mapper_args__ = {'polymorphic_identity': 'purchaselineitem'}
784
    price           = Column(Numeric)
785
    def __init__(self, transaction, amount, item, quantity, price, wholesale):
786
        SubTransaction.__init__(self, transaction, amount, item.id, quantity, wholesale)
787
        self.price = price
788
789
    @classmethod
790
    def quantity_by_period(cls, period, start=None, end=None):
791
        r = DBSession.query(cls.quantity.label('summable'), event.Event.timestamp)\
792
                     .join(Transaction)\
793
                     .join(event.Event)\
794
                     .filter(event.Event.deleted==False)\
795 View Code Duplication
                     .order_by(event.Event.timestamp)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
796
        if start:
797
            r = r.filter(event.Event.timestamp>=start.replace(tzinfo=None))
798
        if end:
799
            r = r.filter(event.Event.timestamp<end.replace(tzinfo=None))
800
        return utility.group(r.all(), period)
801
802
    @classmethod
803
    def virtual_revenue_by_period(cls, period, start=None, end=None):
804
        r = DBSession.query(cls.amount.label('summable'), event.Event.timestamp)\
805
                     .join(Transaction)\
806
                     .join(event.Event)\
807
                     .filter(event.Event.deleted==False)\
808
                     .order_by(event.Event.timestamp)
809
        if start:
810
            r = r.filter(event.Event.timestamp>=start.replace(tzinfo=None))
811
        if end:
812
            r = r.filter(event.Event.timestamp<end.replace(tzinfo=None))
813
        return utility.group(r.all(), period)
814
815
    @classmethod
816
    def profit_on_sales(cls, start=None, end=None):
817
        r = DBSession.query(func.sum(cls.amount-(cls.wholesale*cls.quantity)).label('p'))\
818
                        .join(Transaction)\
819
                        .join(event.Event)\
820
                        .filter(event.Event.deleted==False)
821
        if start:
822
            r = r.filter(event.Event.timestamp>=start.replace(tzinfo=None))
823
        if end:
824
            r = r.filter(event.Event.timestamp<end.replace(tzinfo=None))
825
826
        return r.one().p or Decimal(0.0)
827
828
    @classmethod
829
    def item_sale_quantities(cls, item_id):
830
        return DBSession.query(cls, event.Event)\
831
                        .join(Transaction)\
832
                        .join(event.Event)\
833
                        .filter(event.Event.deleted==False)\
834
                        .filter(cls.item_id==int(item_id))\
835
                        .order_by(event.Event.timestamp).all()
836
837
838
# This is slowww:
839
# @property
840
# def __number_sold(self):
841
#     return object_session(self).query(func.sum(PurchaseLineItem.quantity).label('c'))\
842
#                                .join(Transaction)\
843
#                                .join(event.Event)\
844
#                                .filter(PurchaseLineItem.item_id==self.id)\
845
#                                .filter(event.Event.deleted==False).one().c
846
# item.Item.number_sold = __number_sold
847
848
849
class RestockLineItem(SubTransaction):
850
    __mapper_args__ = {'polymorphic_identity': 'restocklineitem'}
851
    def __init__(self,
852
                 transaction,
853
                 amount,
854
                 item,
855
                 quantity,
856
                 wholesale,
857
                 coupon,
858
                 sales_tax,
859
                 bottle_deposit):
860
        SubTransaction.__init__(self, transaction, amount, item.id, quantity, wholesale)
861
        self.coupon_amount = coupon
862
        self.sales_tax = sales_tax
863
        self.bottle_deposit = bottle_deposit
864
865
866
class RestockLineBox(SubTransaction):
867
    __mapper_args__ = {'polymorphic_identity': 'restocklinebox'}
868
    box_id          = Column(Integer, ForeignKey("boxes.id"), nullable=True)
869
870
    box             = relationship(box.Box, backref="subtransactions")
871
872
    def __init__(self,
873
                 transaction,
874
                 amount,
875
                 box,
876
                 quantity,
877
                 wholesale,
878
                 coupon,
879
                 sales_tax,
880
                 bottle_deposit):
881
        self.transaction_id = transaction.id
882
        self.amount = amount
883
        self.box_id = box.id
884
        self.quantity = quantity
885
        self.wholesale = wholesale
886
        self.coupon_amount = coupon
887
        self.sales_tax = sales_tax
888
        self.bottle_deposit = bottle_deposit
889
890
891
class InventoryLineItem(SubTransaction):
892
    __mapper_args__    = {'polymorphic_identity': 'inventorylineitem'}
893
    quantity_predicted = synonym(SubTransaction.quantity)
894
    quantity_counted   = Column(Integer)
895
896
    def __init__(self, transaction, amount, item, quantity_predicted, quantity_counted, wholesale):
897
        SubTransaction.__init__(self, transaction, amount, item.id, quantity_predicted, wholesale)
898
        self.quantity_counted = quantity_counted
899
900
901
902
################################################################################
903
## SUBSUB TRANSACTIONS
904
################################################################################
905
906
# This is for tracking which items were in which boxes when we restocked
907
908
class SubSubTransaction(Base):
909
    __tablename__      = "subsubtransactions"
910
911
    id                 = Column(Integer, primary_key=True, nullable=False)
912
    subtransaction_id  = Column(Integer, ForeignKey("subtransactions.id"), nullable=False)
913
    type               = Column(Enum("restocklineboxitem",
914
                                     name="subsubtransaction_type"), nullable=False)
915
    item_id            = Column(Integer, ForeignKey("items.id"), nullable=True)
916
    quantity           = Column(Integer, nullable=False)
917
918
    subtransaction     = relationship(SubTransaction, backref="subsubtransactions", cascade="all")
919
    item               = relationship(item.Item, backref="subsubtransactions")
920
921
    __mapper_args__    = {'polymorphic_on': type}
922
923
    def __init__(self, subtransaction, item_id, quantity):
924
        self.subtransaction_id = subtransaction.id
925
        self.item_id = item_id
926
        self.quantity = quantity
927
928
    def __getattr__(self, name):
929
        if name == 'deleted':
930
            return self.subtransaction.transaction.event.deleted
931
        else:
932
            raise AttributeError
933
934
    @classmethod
935
    @limitable_all
936
    def all_item(cls, item_id):
937
        return DBSession.query(cls)\
938
                        .join(SubTransaction)\
939
                        .join(Transaction)\
940
                        .join(event.Event)\
941
                        .filter(cls.item_id == item_id)\
942
                        .filter(event.Event.deleted==False)\
943
                        .order_by(cls.id)
944
945
946
class RestockLineBoxItem(SubSubTransaction):
947
    __mapper_args__ = {'polymorphic_identity': 'restocklineboxitem'}
948
    def __init__(self, subtransaction, item, quantity):
949
        SubSubTransaction.__init__(self, subtransaction, item.id, quantity)
950
951
952
953