Completed
Push — master ( 072bc2...af8208 )
by Pat
01:15
created

RestockLineBox.__init__()   D

Complexity

Conditions 1

Size

Total Lines 17

Duplication

Lines 1
Ratio 5.88 %

Importance

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