Completed
Push — master ( 0da61a...63f959 )
by
unknown
01:02
created

__number_of_purchases()   A

Complexity

Conditions 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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