class Postage(DB.Model): """Model for representing postage for one or more tickets.""" __tablename__ = 'postage' object_id = DB.Column(DB.Integer, primary_key=True) paid = DB.Column(DB.Boolean(), default=False, nullable=False) postage_type = DB.Column(DB.Unicode(50), nullable=False) price = DB.Column(DB.Integer(), nullable=False) address = DB.Column(DB.Unicode(200), nullable=True) cancelled = DB.Column(DB.Boolean(), default=False, nullable=False) posted = DB.Column(DB.Boolean(), default=False, nullable=False) tickets = DB.relationship('Ticket', secondary=POSTAGE_TICKET_LINK, backref=DB.backref('postage', lazy=False, uselist=False), lazy='dynamic') owner_id = DB.Column(DB.Integer, DB.ForeignKey('user.object_id'), nullable=True) owner = DB.relationship('User', backref=DB.backref('postage_entries', lazy='dynamic')) def __init__(self, owner, postage_option, tickets, address=None): self.owner = owner self.postage_type = postage_option.name self.price = postage_option.price self.address = address self.tickets = tickets if self.price == 0: self.paid = True
class AdminFee(DB.Model): """Model for representing an administration fee.""" __tablename__ = 'admin_fee' object_id = DB.Column(DB.Integer, primary_key=True) amount = DB.Column(DB.Integer(), nullable=False) reason = DB.Column(DB.UnicodeText(), nullable=False) paid = DB.Column(DB.Boolean(), nullable=False, default=False) charged_to_id = DB.Column(DB.Integer, DB.ForeignKey('user.object_id'), nullable=False) charged_to = DB.relationship('User', backref=DB.backref('admin_fees_charged', lazy='dynamic'), foreign_keys=[charged_to_id]) charged_by_id = DB.Column(DB.Integer, DB.ForeignKey('user.object_id'), nullable=False) charged_by = DB.relationship('User', backref=DB.backref('admin_fees_created', lazy='dynamic'), foreign_keys=[charged_by_id]) def __init__(self, amount, reason, charged_to, charged_by): self.amount = amount self.reason = reason self.charged_to = charged_to self.charged_by = charged_by def __repr__(self): return '<AdminFee {0}: £{1}>'.format(self.object_id, self.amount_pounds) @property def amount_pounds(self): """Get the fee amount as a string of pounds and pence.""" amount = '{0:03d}'.format(self.amount) return amount[:-2] + '.' + amount[-2:] def mark_as_paid(self): """Email the creator of this fee when it has been paid.""" self.paid = True if "Ticket Upgrade:" in self.reason: possible_tickets = [int(x) for x in self.reason[16:].split(',')] for ticket in self.charged_to.active_tickets: if ticket.object_id in possible_tickets: ticket.add_note('Upgrade') else: APP.email_manager.send_template(self.charged_by.email, 'Administration fee paid.', 'admin_fee_paid.email', fee=self)
class PurchaseGroup(DB.Model): """Model for a purchase group to allow pooling allowances.""" __tablename__ = 'purchase_group' object_id = DB.Column(DB.Integer, primary_key=True) code = DB.Column(DB.Unicode(10), unique=True, nullable=False) leader_id = DB.Column(DB.Integer, DB.ForeignKey('user.object_id'), nullable=False) leader = DB.relationship('User', foreign_keys=[leader_id]) members = DB.relationship('User', secondary=GROUP_MEMBER_LINK, backref=DB.backref('purchase_group', lazy=False, uselist=False), lazy='dynamic') disbanded = DB.Column(DB.Boolean(), default=False, nullable=False) purchased = DB.Column(DB.Boolean(), default=False, nullable=False) def __init__(self, leader): self.leader = leader self.members = [leader] self.code = util.generate_key(10) def __repr__(self): return '<PurchaseGroup({0}): {1} tickets, £{2}>'.format( self.object_id, self.total_requested, self.total_value_pounds) @property def total_value(self): """Get the total value of tickets requested by this group in pence.""" return sum(request.value for request in self.requests) @property def total_value_pounds(self): """Get the total value of this group in pounds and pence.""" value = '{0:03d}'.format(self.total_value) return value[:-2] + '.' + value[-2:] @property def total_requested(self): """Get the total number of tickets requested by this group.""" return sum(request.number_requested for request in self.requests) @property def total_guest_tickets_requested(self): """Get the total number of guest tickets requested by this group.""" return sum(request.number_requested for request in self.requests if request.ticket_type.counts_towards_guest_limit) @staticmethod def get_by_code(code): """Get a purchase group object by its code.""" group = PurchaseGroup.query.filter(PurchaseGroup.code == code).first() if not group: return None return group
class Ticket(DB.Model): """Model for tickets.""" __tablename__ = 'ticket' object_id = DB.Column(DB.Integer, primary_key=True) ticket_type = DB.Column(DB.Unicode(50), nullable=False) paid = DB.Column(DB.Boolean(), default=False, nullable=False) entered = DB.Column(DB.Boolean(), default=False, nullable=False) cancelled = DB.Column(DB.Boolean(), default=False, nullable=False) price_ = DB.Column(DB.Integer(), nullable=False) note = DB.Column(DB.UnicodeText(), nullable=True) expires = DB.Column(DB.DateTime(), nullable=True) barcode = DB.Column(DB.Unicode(20), unique=True, nullable=True) claim_code = DB.Column( DB.Unicode(17), # 3 groups of 5 digits separated by dashes nullable=True) claims_made = DB.Column(DB.Integer, nullable=False, default=0) owner_id = DB.Column(DB.Integer, DB.ForeignKey('user.object_id'), nullable=False) owner = DB.relationship('User', backref=DB.backref('tickets', lazy='dynamic', order_by=b'Ticket.cancelled'), foreign_keys=[owner_id]) holder_id = DB.Column(DB.Integer, DB.ForeignKey('user.object_id'), nullable=True) holder = DB.relationship('User', backref=DB.backref('held_ticket', uselist=False), foreign_keys=[holder_id]) holder_name = DB.Column(DB.String(60), nullable=True, default="Unassigned") def __init__(self, owner, ticket_type, price): self.owner = owner self.ticket_type = ticket_type self.price = price self.expires = (datetime.datetime.utcnow() + APP.config['TICKET_EXPIRY_TIME']) self.claim_code = '-'.join( util.generate_key(5, string.digits) for _ in xrange(3)).decode('utf-8') def __repr__(self): return '<Ticket {0} owned by {1} ({2})>'.format( self.object_id, self.owner.full_name, self.owner.object_id) def can_be_cancelled(self): if datetime.datetime.utcnow() < datetime.datetime( 2018, 5, 11) and not self.cancelled and not self.paid: return True return False def can_be_resold(self): return False def can_be_claimed(self): if self.status == 'Awaiting ticket holder.': return True return False def can_be_reclaimed(self): # if self.paid and datetime.datetime.utcnow()<datetime.datetime(2018,5,11) and not self.cancelled and self.holder_id==self.owner_id: # return True return False def has_holder(self): if not self.holder_id == None: return True return False def can_be_paid_for(self): if self.paid == 0: return True return False def can_be_collected(self): if self.paid: return True return False def is_assigned(self): if self.holder_name == 'Unassigned': return False return True @staticmethod def get_by_claim_code(code): return Ticket.query.filter_by(claim_code=code).first() @property def price_pounds(self): """Get the price of this ticket as a string of pounds and pence.""" price = '{0:03d}'.format(self.price) return price[:-2] + '.' + price[-2:] @property def transaction(self): """Get the transaction this ticket was paid for in.""" for transaction_item in self.transaction_items: if transaction_item.transaction.paid: return transaction_item.transaction return None @property def payment_method(self): """Get the payment method for this ticket.""" transaction = self.transaction if transaction: return transaction.payment_method else: return 'Unknown Payment Method' @property def price(self): """Get the price of the ticket.""" return self.price_ @property def status(self): """Get the status of this ticket.""" if self.cancelled: return 'Cancelled.' elif not self.paid: return 'Awaiting payment. Expires {0}.'.format( self.expires.strftime('%H:%M %d/%m/%Y')) elif self.entered: return 'Used for entry.' elif self.collected and self.holder: return 'Ticket Sent to {0}.'.format(self.holder.full_name) elif self.collected: return 'Collected as {0}.'.format(self.barcode) # elif self.holder is None: # return 'Awaiting ticket holder.' elif self.holder_name == 'Unassigned': return 'Awaiting ticket holder.' elif APP.config['REQUIRE_USER_PHOTO']: if not self.holder.photo.verified: return 'Awaiting verification of holder photo.' else: # return 'Held by {0}.'.format(self.holder.full_name) return 'Assigned to {0}.'.format(self.holder_name) @price.setter def price(self, value): """Set the price of the ticket.""" self.price_ = max(value, 0) if self.price_ == 0: self.mark_as_paid() @hybrid.hybrid_property def collected(self): """Has this ticket been assigned a barcode.""" return self.barcode != None # pylint: disable=singleton-comparison def mark_as_paid(self): """Mark the ticket as paid, and clear any expiry.""" self.paid = True self.expires = None def add_note(self, note): """Add a note to the ticket.""" if not note.endswith('\n'): note = note + '\n' if self.note is None: self.note = note else: self.note = self.note + note @staticmethod def count(): """How many tickets have been sold.""" # TODO return Ticket.query.filter(Ticket.cancelled == False).count() # pylint: disable=singleton-comparison @staticmethod def write_csv_header(csv_writer): """Write the header of a CSV export file.""" csv_writer.writerow([ 'Ticket ID', 'Ticket Type', 'Paid', 'Collected', 'Entered', 'Cancelled', 'Price (Pounds)', 'Holder\'s Name', 'Notes', 'Expires', 'Barcode', 'Owner\' User ID', 'Owner\'s Name', ]) def write_csv_row(self, csv_writer): """Write this object as a row in a CSV export file.""" csv_writer.writerow([ self.object_id, self.ticket_type, 'Yes' if self.paid else 'No', 'Yes' if self.collected else 'No', 'Yes' if self.entered else 'No', 'Yes' if self.cancelled else 'No', self.price_pounds, self.holder.full_name.encode('utf-8') if self.holder is not None else 'N/A', self.note, self.expires.strftime('%Y-%m-%d %H:%M:%S') if self.expires is not None else 'N/A', self.barcode if self.barcode is not None else 'N/A', self.owner_id, self.owner.full_name.encode('utf-8'), ])
class Battels(DB.Model): """Model for battels charges for current students.""" __tablename__ = 'battels' object_id = DB.Column(DB.Integer, primary_key=True) battels_id = DB.Column(DB.Unicode(10), unique=True, nullable=True) email = DB.Column(DB.Unicode(120), unique=True, nullable=True) title = DB.Column(DB.Unicode(10), nullable=True) surname = DB.Column(DB.Unicode(60), nullable=True) forenames = DB.Column(DB.Unicode(60), nullable=True) michaelmas_charge = DB.Column(DB.Integer(), default=0, nullable=False) hilary_charge = DB.Column(DB.Integer(), default=0, nullable=False) manual = DB.Column(DB.Boolean(), default=False, nullable=False) def __init__(self, battelsid=None, email=None, title=None, surname=None, forenames=None, manual=False): self.battelsid = battelsid self.email = email self.title = title self.surname = surname self.forenames = forenames self.manual = manual def __repr__(self): return '<Battels {0}: {1}>'.format(self.object_id, self.battelsid) @property def michaelmas_charge_pounds(self): """Get the amount charged in Michaelmas as a pounds/pence string.""" michaelmas_charge = '{0:03d}'.format(self.michaelmas_charge) return michaelmas_charge[:-2] + '.' + michaelmas_charge[-2:] @property def hilary_charge_pounds(self): """Get the amount charged in Hilary as a pounds/pence string.""" hilary_charge = '{0:03d}'.format(self.hilary_charge) return hilary_charge[:-2] + '.' + hilary_charge[-2:] def charge(self, amount, term): """Apply a charge to this battels account.""" if term == 'MTHT': # Integer division fails for negative numbers (i.e. refunds), as the # number is rounded the wrong way. Instead, we do floating point # division, and truncate. half = int(amount / 2.0) self.michaelmas_charge += half self.hilary_charge += amount - half elif term == 'MT': self.michaelmas_charge += amount elif term == 'HT': self.hilary_charge += amount else: raise ValueError( 'Term "{0}" cannot be charged to battels'.format(term)) def refund(self, amount, term): """Refund a ticket and mark it as cancelled.""" if APP.config['CURRENT_TERM'] == 'MT': if term == 'MTHT': half = amount // 2 self.michaelmas_charge -= half self.hilary_charge -= amount - half elif term == 'MT': self.michaelmas_charge -= amount elif term == 'HT': self.hilary_charge -= amount elif APP.config['CURRENT_TERM'] == 'HT': self.hilary_charge -= amount else: raise ValueError( 'Can\'t refund battels tickets in the current term')
class Voucher(DB.Model): """Model for a discount voucher.""" __tablename__ = 'voucher' object_id = DB.Column(DB.Integer, primary_key=True) code = DB.Column(DB.Unicode(30), nullable=False) expires = DB.Column(DB.DateTime(), nullable=True) discount_type = DB.Column(DB.Enum('Fixed Price', 'Fixed Discount', 'Percentage Discount'), nullable=False) discount_value = DB.Column(DB.Integer(), nullable=False) applies_to = DB.Column(DB.Enum('Ticket', 'Transaction'), nullable=False) single_use = DB.Column(DB.Boolean(), nullable=False) used = DB.Column(DB.Boolean(), default=False, nullable=True) used_by_id = DB.Column(DB.Integer, DB.ForeignKey('user.object_id'), nullable=True) used_by = DB.relationship('User', backref=DB.backref('vouchers_used', lazy='dynamic')) def __init__(self, code, expires, discount_type, discount_value, applies_to, single_use): if discount_type not in [ 'Fixed Price', 'Fixed Discount', 'Percentage Discount' ]: raise ValueError( '{0} is not a valid discount type'.format(discount_type)) if applies_to not in ['Ticket', 'Transaction']: raise ValueError( '{0} is not a valid application'.format(applies_to)) self.code = code self.discount_type = discount_type self.discount_value = discount_value self.applies_to = applies_to self.single_use = single_use if isinstance(expires, datetime.timedelta): self.expires = datetime.datetime.utcnow() + expires else: self.expires = expires def __repr__(self): return '<Voucher: {0}/{1}>'.format(self.object_id, self.code) @staticmethod def get_by_code(code): """Get an Announcement object by a voucher code.""" return Voucher.query.filter(Voucher.code == code).first() def apply(self, tickets, user): """Apply the voucher to a set of tickets. Checks if the voucher can be used, and applies its discount to the tickets. Args: tickets: (list(Ticket)) list of tickets to apply the voucher to user: (User) user who is using the voucher Returns: (bool, list(tickets), str/None) whether the voucher was applied, the mutated tickets, and an error message """ if self.single_use and self.used: return (False, tickets, 'Voucher has already been used.') if (self.expires is not None and self.expires < datetime.datetime.utcnow()): return (False, tickets, 'Voucher has expired.') self.used = True if self.single_use: self.used_by = user if self.applies_to == 'Ticket': tickets[0] = self.apply_to_ticket(tickets[0]) return (True, tickets, None) else: return (True, [self.apply_to_ticket(t) for t in tickets], None) def apply_to_ticket(self, ticket): """Apply the voucher to a single ticket. Recalculates the price of the ticket, and notes on the ticket that a voucher was used Args: ticket: (Ticket) the ticket to apply the voucher to Returns: (ticket) the mutated ticket """ if self.discount_type == 'Fixed Price': ticket.price = self.discount_value elif self.discount_type == 'Fixed Discount': ticket.price = ticket.price - self.discount_value else: ticket.price = ticket.price * (100 - self.discount_value) / 100 ticket.add_note('Used voucher {0}/{1}'.format(self.object_id, self.code)) return ticket