Completed
Push — master ( 542682...b61ac1 )
by
unknown
59s
created

chezbetty.models.__get_restock_events()   A

Complexity

Conditions 1

Size

Total Lines 7

Duplication

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