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