Completed
Push — master ( ba470c...1324e2 )
by
unknown
01:42
created

__get_events_by_type()   C

Complexity

Conditions 7

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

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