class Assignee(BaseMixin, db.Model): __tablename__ = 'assignee' __table_args__ = (db.UniqueConstraint('line_item_id', 'current'), db.CheckConstraint("current != '0'", 'assignee_current_check')) # lastuser id user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) user = db.relationship('User', backref=db.backref('assignees', cascade='all, delete-orphan')) line_item_id = db.Column(None, db.ForeignKey('line_item.id'), nullable=False) line_item = db.relationship('LineItem', backref=db.backref( 'assignees', cascade='all, delete-orphan', lazy='dynamic')) fullname = db.Column(db.Unicode(80), nullable=False) #: Unvalidated email address email = db.Column(db.Unicode(254), nullable=False) #: Unvalidated phone number phone = db.Column(db.Unicode(16), nullable=True) details = db.Column(JsonDict, nullable=False, default={}) current = db.Column(db.Boolean, nullable=True)
class PaymentTransaction(BaseMixin, db.Model): """ Models transactions made with a customer. A transaction can either be of type 'Payment', 'Refund', 'Credit', """ __tablename__ = 'payment_transaction' __uuid_primary_key__ = True customer_order_id = db.Column(None, db.ForeignKey('customer_order.id'), nullable=False) order = db.relationship(Order, backref=db.backref('transactions', cascade='all, delete-orphan', lazy="dynamic")) online_payment_id = db.Column(None, db.ForeignKey('online_payment.id'), nullable=True) online_payment = db.relationship(OnlinePayment, backref=db.backref( 'transactions', cascade='all, delete-orphan')) amount = db.Column(db.Numeric, nullable=False) currency = db.Column(db.Unicode(3), nullable=False) transaction_type = db.Column(db.Integer, default=TRANSACTION_TYPE.PAYMENT, nullable=False) transaction_method = db.Column(db.Integer, default=TRANSACTION_METHOD.ONLINE, nullable=False)
class Invoice(UuidMixin, BaseMixin, db.Model): __tablename__ = 'invoice' __uuid_primary_key__ = True __table_args__ = (db.UniqueConstraint('organization_id', 'invoice_no'),) status = db.Column(db.SmallInteger, default=INVOICE_STATUS.DRAFT, nullable=False) invoicee_name = db.Column(db.Unicode(255), nullable=True) invoicee_company = db.Column(db.Unicode(255), nullable=True) invoicee_email = db.Column(db.Unicode(254), nullable=True) invoice_no = db.Column(db.Integer(), nullable=True) invoiced_at = db.Column(db.DateTime, nullable=True) street_address_1 = db.Column(db.Unicode(255), nullable=True) street_address_2 = db.Column(db.Unicode(255), nullable=True) city = db.Column(db.Unicode(255), nullable=True) state = db.Column(db.Unicode(255), nullable=True) # ISO 3166-2 code. Eg: KA for Karnataka state_code = db.Column(db.Unicode(3), nullable=True) # ISO country code country_code = db.Column(db.Unicode(2), nullable=True) postcode = db.Column(db.Unicode(8), nullable=True) # GSTIN in the case of India buyer_taxid = db.Column(db.Unicode(255), nullable=True) seller_taxid = db.Column(db.Unicode(255), nullable=True) customer_order_id = db.Column(None, db.ForeignKey('customer_order.id'), nullable=False, index=True) order = db.relationship('Order', backref=db.backref('invoices', cascade='all, delete-orphan')) # An invoice may be associated with a different organization as compared to its order # to allow for the following use case. An invoice may be issued by a parent entity, while the order is booked through # the child entity. organization_id = db.Column(None, db.ForeignKey('organization.id'), nullable=False) organization = db.relationship('Organization', backref=db.backref('invoices', cascade='all, delete-orphan', lazy='dynamic')) def __init__(self, *args, **kwargs): organization = kwargs.get('organization') country_code = kwargs.get('country_code') if not country_code: # Default to India country_code = u'IN' if not organization: raise ValueError(u"Invoice MUST be initialized with an organization") self.invoiced_at = datetime.utcnow() self.invoice_no = gen_invoice_no(organization, country_code, self.invoiced_at) super(Invoice, self).__init__(*args, **kwargs) @property def is_final(self): return self.status == INVOICE_STATUS.FINAL @validates('invoicee_name', 'invoicee_company', 'invoicee_email', 'invoice_no', 'invoiced_at', 'street_address_1', 'street_address_2', 'city', 'state', 'state_code', 'country_code', 'postcode', 'buyer_taxid', 'seller_taxid', 'customer_order_id', 'organization_id') def validate_immutable_final_invoice(self, key, val): if self.status == INVOICE_STATUS.FINAL: raise ValueError("`{attr}` cannot be modified in a finalized invoice".format(attr=key)) return val
class OnlinePayment(BaseMixin, db.Model): """ Represents payments made through a payment gateway. Supports Razorpay only. """ __tablename__ = 'online_payment' __uuid_primary_key__ = True customer_order_id = db.Column(None, db.ForeignKey('customer_order.id'), nullable=False) order = db.relationship(Order, backref=db.backref('online_payments', cascade='all, delete-orphan')) # Payment id issued by the payment gateway pg_paymentid = db.Column(db.Unicode(80), nullable=False, unique=True) # Payment status issued by the payment gateway pg_payment_status = db.Column(db.Integer, nullable=False) confirmed_at = db.Column(db.DateTime, nullable=True) failed_at = db.Column(db.DateTime, nullable=True) def confirm(self): """Confirms a payment, sets confirmed_at and pg_payment_status.""" self.confirmed_at = func.utcnow() self.pg_payment_status = RAZORPAY_PAYMENT_STATUS.CAPTURED def fail(self): """Fails a payment, sets failed_at.""" self.pg_payment_status = RAZORPAY_PAYMENT_STATUS.FAILED self.failed_at = func.utcnow()
class Organization(ProfileBase, db.Model): __tablename__ = 'organization' __table_args__ = (db.UniqueConstraint('contact_email'), ) # The currently used fields in details are address(html) # cin (Corporate Identity Number) or llpin (Limited Liability Partnership Identification Number), # pan, service_tax_no, support_email, # logo (image url), refund_policy (html), ticket_faq (html), website (url) details = db.Column(JsonDict, nullable=False, server_default='{}') contact_email = db.Column(db.Unicode(254), nullable=False) # This is to allow organizations to have their orders invoiced by the parent organization invoicer_id = db.Column(None, db.ForeignKey('organization.id'), nullable=True) invoicer = db.relationship('Organization', remote_side='Organization.id', backref=db.backref('subsidiaries', cascade='all, delete-orphan', lazy='dynamic')) def permissions(self, user, inherited=None): # import IPython; IPython.embed(); perms = super(Organization, self).permissions(user, inherited) if self.userid in user.organizations_owned_ids(): perms.add('org_admin') return perms
class PaymentTransaction(BaseMixin, db.Model): """ Models transactions made with a customer. A transaction can either be of type 'Payment', 'Refund', 'Credit', """ __tablename__ = 'payment_transaction' __uuid_primary_key__ = True customer_order_id = db.Column(None, db.ForeignKey('customer_order.id'), nullable=False) order = db.relationship(Order, backref=db.backref('transactions', cascade='all, delete-orphan', lazy="dynamic")) online_payment_id = db.Column(None, db.ForeignKey('online_payment.id'), nullable=True) online_payment = db.relationship(OnlinePayment, backref=db.backref( 'transactions', cascade='all, delete-orphan')) amount = db.Column(db.Numeric, nullable=False) currency = db.Column(db.Unicode(3), nullable=False) transaction_type = db.Column(db.Integer, default=TRANSACTION_TYPE.PAYMENT, nullable=False) transaction_method = db.Column(db.Integer, default=TRANSACTION_METHOD.ONLINE, nullable=False) # Eg: reference number for a bank transfer transaction_ref = db.Column(db.Unicode(80), nullable=True) refunded_at = db.Column(db.DateTime, nullable=True) internal_note = db.Column(db.Unicode(250), nullable=True) refund_description = db.Column(db.Unicode(250), nullable=True) note_to_user = MarkdownColumn('note_to_user', nullable=True) # Refund id issued by the payment gateway pg_refundid = db.Column(db.Unicode(80), nullable=True, unique=True)
class DiscountPolicy(BaseScopedNameMixin, db.Model): """ Consists of the discount rules applicable on items """ __tablename__ = 'discount_policy' __uuid_primary_key__ = True __table_args__ = (db.UniqueConstraint('organization_id', 'name'), db.CheckConstraint( 'percentage > 0 and percentage <= 100', 'discount_policy_percentage_check')) organization_id = db.Column(None, db.ForeignKey('organization.id'), nullable=False) organization = db.relationship(Organization, backref=db.backref( 'discount_policies', cascade='all, delete-orphan')) parent = db.synonym('organization') discount_type = db.Column(db.Integer, default=DISCOUNT_TYPE.AUTOMATIC, nullable=False) # Minimum number of a particular item that needs to be bought for this discount to apply item_quantity_min = db.Column(db.Integer, default=1, nullable=False) percentage = db.Column(db.Integer, nullable=True) # price-based discount is_price_based = db.Column(db.Boolean, default=False, nullable=False) items = db.relationship('Item', secondary=item_discount_policy) @cached_property def is_automatic(self): return self.discount_type == DISCOUNT_TYPE.AUTOMATIC @cached_property def is_coupon(self): return self.discount_type == DISCOUNT_TYPE.COUPON
class DiscountCoupon(IdMixin, db.Model): __tablename__ = 'discount_coupon' __uuid_primary_key__ = True __table_args__ = (db.UniqueConstraint('discount_policy_id', 'code'),) def __init__(self, *args, **kwargs): self.id = uuid1mc() super(DiscountCoupon, self).__init__(*args, **kwargs) code = db.Column(db.Unicode(100), nullable=False, default=generate_coupon_code) usage_limit = db.Column(db.Integer, nullable=False, default=1) used_count = db.Column(db.Integer, nullable=False, default=0) discount_policy_id = db.Column(None, db.ForeignKey('discount_policy.id'), nullable=False) discount_policy = db.relationship(DiscountPolicy, backref=db.backref('discount_coupons', cascade='all, delete-orphan'))
class Category(BaseScopedNameMixin, db.Model): __tablename__ = 'category' __table_args__ = (db.UniqueConstraint('item_collection_id', 'name'), db.UniqueConstraint('item_collection_id', 'seq')) item_collection_id = db.Column(None, db.ForeignKey('item_collection.id'), nullable=False) seq = db.Column(db.Integer, nullable=False) item_collection = db.relationship(ItemCollection, backref=db.backref( 'categories', cascade='all, delete-orphan')) parent = db.synonym('item_collection')
class OrderSession(BaseMixin, db.Model): """ Records the referrer and utm headers for an order """ __tablename__ = 'order_session' __uuid_primary_key__ = True customer_order_id = db.Column(None, db.ForeignKey('customer_order.id'), nullable=False, index=True, unique=False) order = db.relationship(Order, backref=db.backref('session', cascade='all, delete-orphan', uselist=False)) referrer = db.Column(db.Unicode(2083), nullable=True) # Google Analytics parameters utm_source = db.Column(db.Unicode(250), nullable=False, default=u'', index=True) utm_medium = db.Column(db.Unicode(250), nullable=False, default=u'', index=True) utm_term = db.Column(db.Unicode(250), nullable=False, default=u'') utm_content = db.Column(db.Unicode(250), nullable=False, default=u'') utm_id = db.Column(db.Unicode(250), nullable=False, default=u'', index=True) utm_campaign = db.Column(db.Unicode(250), nullable=False, default=u'', index=True) # Google click id (for AdWords) gclid = db.Column(db.Unicode(250), nullable=False, default=u'', index=True)
self.status = LINE_ITEM_STATUS.VOID self.cancelled_at = func.utcnow() def is_cancellable(self): return self.is_confirmed and (datetime.datetime.utcnow() < self.item.cancellable_until if self.item.cancellable_until else True) @classmethod def get_max_seq(cls, order): return db.session.query(func.max(LineItem.line_item_seq)).filter(LineItem.order == order).scalar() Order.confirmed_line_items = db.relationship(LineItem, lazy='dynamic', primaryjoin=db.and_( LineItem.customer_order_id == Order.id, LineItem.status == LINE_ITEM_STATUS.CONFIRMED ) ) Order.confirmed_and_cancelled_line_items = db.relationship(LineItem, lazy='dynamic', primaryjoin=db.and_( LineItem.customer_order_id == Order.id, LineItem.status.in_([LINE_ITEM_STATUS.CONFIRMED, LINE_ITEM_STATUS.CANCELLED]) ) ) Order.initial_line_items = db.relationship(LineItem,
class LineItem(BaseMixin, db.Model): """ Note: Line Items MUST NOT be deleted. They must only be cancelled. """ __tablename__ = 'line_item' __uuid_primary_key__ = True __table_args__ = (db.UniqueConstraint('customer_order_id', 'line_item_seq'), ) customer_order_id = db.Column(None, db.ForeignKey('customer_order.id'), nullable=False, index=True, unique=False) order = db.relationship(Order, backref=db.backref('line_items', cascade='all, delete-orphan')) # line_item_seq is the relative number of the line item per order. line_item_seq = db.Column(db.Integer, nullable=False) item_id = db.Column(None, db.ForeignKey('item.id'), nullable=False, index=True, unique=False) item = db.relationship(Item, backref=db.backref('line_items', cascade='all, delete-orphan')) discount_policy_id = db.Column(None, db.ForeignKey('discount_policy.id'), nullable=True, index=True, unique=False) discount_policy = db.relationship('DiscountPolicy', backref=db.backref('line_items')) discount_coupon_id = db.Column(None, db.ForeignKey('discount_coupon.id'), nullable=True, index=True, unique=False) discount_coupon = db.relationship('DiscountCoupon', backref=db.backref('line_items')) base_amount = db.Column(db.Numeric, default=Decimal(0), nullable=False) discounted_amount = db.Column(db.Numeric, default=Decimal(0), nullable=False) final_amount = db.Column(db.Numeric, default=Decimal(0), nullable=False) status = db.Column(db.Integer, default=LINE_ITEM_STATUS.PURCHASE_ORDER, nullable=False) ordered_at = db.Column(db.DateTime, nullable=True) cancelled_at = db.Column(db.DateTime, nullable=True) @classmethod def calculate(cls, line_item_dicts, coupons=[]): """ Returns line item tuples with the respective base_amount, discounted_amount, final_amount, discount_policy and discount coupon populated """ item_line_items = {} line_items = [] for line_item_dict in line_item_dicts: item = Item.query.get(line_item_dict['item_id']) if not item_line_items.get(unicode(item.id)): item_line_items[unicode(item.id)] = [] item_line_items[unicode(item.id)].append( make_ntuple(item_id=item.id, base_amount=item.current_price().amount)) coupon_list = list(set(coupons)) if coupons else [] discounter = LineItemDiscounter() for item_id in item_line_items.keys(): item_line_items[item_id] = discounter.get_discounted_line_items( item_line_items[item_id], coupon_list) line_items.extend(item_line_items[item_id]) return line_items def confirm(self): self.status = LINE_ITEM_STATUS.CONFIRMED # TODO: assignee = db.relationship(Assignee, primaryjoin=Assignee.line_item == self and Assignee.current == True, uselist=False) # Don't use current_assignee -- we want to imply that there can only be one assignee and the rest are historical (and hence not 'assignees') @property def current_assignee(self): return self.assignees.filter(Assignee.current == True).one_or_none() @property def is_confirmed(self): return self.status == LINE_ITEM_STATUS.CONFIRMED def cancel(self): """ Sets status and cancelled_at. """ self.status = LINE_ITEM_STATUS.CANCELLED self.cancelled_at = func.utcnow() def is_cancellable(self): return self.is_confirmed and ( datetime.datetime.now() < self.item.cancellable_until if self.item.cancellable_until else True)
class DiscountPolicy(BaseScopedNameMixin, db.Model): """ Consists of the discount rules applicable on items `title` has a GIN index to enable trigram matching. """ __tablename__ = 'discount_policy' __uuid_primary_key__ = True __table_args__ = (db.UniqueConstraint('organization_id', 'name'), db.UniqueConstraint('organization_id', 'discount_code_base'), db.CheckConstraint('percentage > 0 and percentage <= 100', 'discount_policy_percentage_check'), db.CheckConstraint('discount_type = 0 or (discount_type = 1 and bulk_coupon_usage_limit IS NOT NULL)', 'discount_policy_bulk_coupon_usage_limit_check')) organization_id = db.Column(None, db.ForeignKey('organization.id'), nullable=False) organization = db.relationship(Organization, backref=db.backref('discount_policies', order_by='DiscountPolicy.created_at.desc()', lazy='dynamic', cascade='all, delete-orphan')) parent = db.synonym('organization') discount_type = db.Column(db.Integer, default=DISCOUNT_TYPE.AUTOMATIC, nullable=False) # Minimum number of a particular item that needs to be bought for this discount to apply item_quantity_min = db.Column(db.Integer, default=1, nullable=False) percentage = db.Column(db.Integer, nullable=True) # price-based discount is_price_based = db.Column(db.Boolean, default=False, nullable=False) discount_code_base = db.Column(db.Unicode(20), nullable=True) secret = db.Column(db.Unicode(50), nullable=True) items = db.relationship('Item', secondary=item_discount_policy) # Coupons generated in bulk are not stored in the database during generation. # This field allows specifying the number of times a coupon, generated in bulk, can be used # This is particularly useful for generating referral discount coupons. For instance, one could generate # a signed coupon and provide it to a user such that the user can share the coupon `n` times # `n` here is essentially bulk_coupon_usage_limit. bulk_coupon_usage_limit = db.Column(db.Integer, nullable=True, default=1) def __init__(self, *args, **kwargs): self.secret = kwargs.get('secret') if kwargs.get('secret') else buid() super(DiscountPolicy, self).__init__(*args, **kwargs) @cached_property def is_automatic(self): return self.discount_type == DISCOUNT_TYPE.AUTOMATIC @cached_property def is_coupon(self): return self.discount_type == DISCOUNT_TYPE.COUPON def gen_signed_code(self, identifier=None): """Generates a signed code in the format discount_code_base.randint.signature""" if not identifier: identifier = buid() signer = Signer(self.secret) key = "{base}.{identifier}".format(base=self.discount_code_base, identifier=identifier) return signer.sign(key) @staticmethod def is_signed_code_format(code): """Checks if the code is in the {x.y.z} format""" return len(code.split('.')) == 3 if code else False @classmethod def get_from_signed_code(cls, code): """Returns a discount policy given a valid signed code, returns None otherwise""" if not cls.is_signed_code_format(code): return None discount_code_base = code.split('.')[0] policy = cls.query.filter_by(discount_code_base=discount_code_base).one_or_none() if not policy: return None signer = Signer(policy.secret) try: signer.unsign(code) return policy except BadSignature: return None @classmethod def make_bulk(cls, discount_code_base, **kwargs): """ Returns a discount policy for the purpose of issuing signed discount coupons in bulk. """ return cls(discount_type=DISCOUNT_TYPE.COUPON, discount_code_base=discount_code_base, secret=buid(), **kwargs)
class LineItem(BaseMixin, db.Model): """ Note: Line Items MUST NOT be deleted. They must only be cancelled. # TODO: Rename this model to `Ticket` """ __tablename__ = 'line_item' __uuid_primary_key__ = True __table_args__ = (db.UniqueConstraint('customer_order_id', 'line_item_seq'), db.UniqueConstraint('previous_id')) # line_item_seq is the relative number of the line item per order. line_item_seq = db.Column(db.Integer, nullable=False) customer_order_id = db.Column(None, db.ForeignKey('customer_order.id'), nullable=False, index=True, unique=False) order = db.relationship(Order, backref=db.backref('line_items', cascade='all, delete-orphan', order_by=line_item_seq, collection_class=ordering_list( 'line_item_seq', count_from=1))) item_id = db.Column(None, db.ForeignKey('item.id'), nullable=False, index=True, unique=False) item = db.relationship(Item, backref=db.backref('line_items', cascade='all, delete-orphan')) previous_id = db.Column(None, db.ForeignKey('line_item.id'), nullable=True, index=True, unique=True) previous = db.relationship( 'LineItem', primaryjoin='line_item.c.id==line_item.c.previous_id', backref=db.backref('revision', uselist=False), remote_side='LineItem.id') discount_policy_id = db.Column(None, db.ForeignKey('discount_policy.id'), nullable=True, index=True, unique=False) discount_policy = db.relationship('DiscountPolicy', backref=db.backref('line_items')) discount_coupon_id = db.Column(None, db.ForeignKey('discount_coupon.id'), nullable=True, index=True, unique=False) discount_coupon = db.relationship('DiscountCoupon', backref=db.backref('line_items')) base_amount = db.Column(db.Numeric, default=Decimal(0), nullable=False) discounted_amount = db.Column(db.Numeric, default=Decimal(0), nullable=False) final_amount = db.Column(db.Numeric, default=Decimal(0), nullable=False) status = db.Column(db.Integer, default=LINE_ITEM_STATUS.PURCHASE_ORDER, nullable=False) ordered_at = db.Column(db.DateTime, nullable=True) cancelled_at = db.Column(db.DateTime, nullable=True) def permissions(self, user, inherited=None): perms = super(LineItem, self).permissions(user, inherited) if self.order.organization.userid in user.organizations_owned_ids(): perms.add('org_admin') return perms @classmethod def calculate(cls, line_items, recalculate=False, coupons=[]): """ Returns line item tuples with the respective base_amount, discounted_amount, final_amount, discount_policy and discount coupon populated If the `recalculate` flag is set to `True`, the line_items will be considered as SQLAlchemy objects. """ item_line_items = {} calculated_line_items = [] coupon_list = list(set(coupons)) if coupons else [] discounter = LineItemDiscounter() # make named tuples for line items, # assign the base_amount for each of them, None if an item is unavailable for line_item in line_items: if recalculate: item = line_item.item # existing line item, use the original base amount base_amount = line_item.base_amount line_item_id = line_item.id else: item = Item.query.get(line_item['item_id']) # new line item, use the current price base_amount = item.current_price( ).amount if item.is_available else None line_item_id = None if not item_line_items.get(unicode(item.id)): item_line_items[unicode(item.id)] = [] item_line_items[unicode(item.id)].append( make_ntuple(item_id=item.id, base_amount=base_amount, line_item_id=line_item_id)) for item_id in item_line_items.keys(): calculated_line_items.extend( discounter.get_discounted_line_items(item_line_items[item_id], coupon_list)) return calculated_line_items def confirm(self): self.status = LINE_ITEM_STATUS.CONFIRMED # TODO: assignee = db.relationship(Assignee, primaryjoin=Assignee.line_item == self and Assignee.current == True, uselist=False) # Don't use current_assignee -- we want to imply that there can only be one assignee and the rest are historical (and hence not 'assignees') @property def current_assignee(self): return self.assignees.filter(Assignee.current == True).one_or_none() @property def is_confirmed(self): return self.status == LINE_ITEM_STATUS.CONFIRMED @property def is_cancelled(self): return self.status == LINE_ITEM_STATUS.CANCELLED @property def is_free(self): return self.final_amount == Decimal('0') def cancel(self): """Sets status and cancelled_at.""" self.status = LINE_ITEM_STATUS.CANCELLED self.cancelled_at = func.utcnow() def make_void(self): self.status = LINE_ITEM_STATUS.VOID self.cancelled_at = func.utcnow() def is_cancellable(self): return self.is_confirmed and ( datetime.datetime.utcnow() < self.item.cancellable_until if self.item.cancellable_until else True) @classmethod def get_max_seq(cls, order): return db.session.query(func.max( LineItem.line_item_seq)).filter(LineItem.order == order).scalar()
class Order(BaseMixin, db.Model): __tablename__ = 'customer_order' __uuid_primary_key__ = True __table_args__ = (db.UniqueConstraint('organization_id', 'invoice_no'), db.UniqueConstraint('access_token')) user_id = db.Column(None, db.ForeignKey('user.id'), nullable=True) user = db.relationship(User, backref=db.backref('orders', cascade='all, delete-orphan')) item_collection_id = db.Column(None, db.ForeignKey('item_collection.id'), nullable=False) item_collection = db.relationship('ItemCollection', backref=db.backref('orders', cascade='all, delete-orphan', lazy='dynamic')) organization_id = db.Column(None, db.ForeignKey('organization.id'), nullable=False) organization = db.relationship('Organization', backref=db.backref('orders', cascade='all, delete-orphan', lazy='dynamic')) status = db.Column(db.Integer, default=ORDER_STATUS.PURCHASE_ORDER, nullable=False) initiated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) paid_at = db.Column(db.DateTime, nullable=True) invoiced_at = db.Column(db.DateTime, nullable=True) cancelled_at = db.Column(db.DateTime, nullable=True) access_token = db.Column(db.Unicode(22), nullable=False, default=buid) buyer_email = db.Column(db.Unicode(254), nullable=False) buyer_fullname = db.Column(db.Unicode(80), nullable=False) buyer_phone = db.Column(db.Unicode(16), nullable=False) # TODO: Deprecate invoice_no, rename to receipt_no instead invoice_no = db.Column(db.Integer, nullable=True) def permissions(self, user, inherited=None): perms = super(Order, self).permissions(user, inherited) if self.organization.userid in user.organizations_owned_ids(): perms.add('org_admin') return perms def confirm_sale(self): """Updates the status to ORDER_STATUS.SALES_ORDER""" for line_item in self.line_items: line_item.confirm() self.invoice_no = gen_invoice_no(self.organization) self.status = ORDER_STATUS.SALES_ORDER self.paid_at = datetime.utcnow() def invoice(self): """Sets invoiced_at, status""" for line_item in self.line_items: line_item.confirm() self.invoiced_at = datetime.utcnow() self.status = ORDER_STATUS.INVOICE def get_amounts(self, line_item_status): """ Calculates and returns the order's base_amount, discounted_amount, final_amount, confirmed_amount as a namedtuple for all the line items with the given status. """ base_amount = Decimal(0) discounted_amount = Decimal(0) final_amount = Decimal(0) confirmed_amount = Decimal(0) for line_item in self.line_items: if line_item.status == line_item_status: base_amount += line_item.base_amount discounted_amount += line_item.discounted_amount final_amount += line_item.final_amount if line_item.is_confirmed: confirmed_amount += line_item.final_amount return order_amounts_ntuple(base_amount, discounted_amount, final_amount, confirmed_amount) @property def is_confirmed(self): return self.status in ORDER_STATUS.CONFIRMED def is_fully_assigned(self): """Checks if all the line items in an order have an assignee""" for line_item in self.get_confirmed_line_items: if not line_item.current_assignee: return False return True
class Order(BaseMixin, db.Model): __tablename__ = 'customer_order' __uuid_primary_key__ = True __table_args__ = (db.UniqueConstraint('organization_id', 'invoice_no'), db.UniqueConstraint('access_token')) user_id = db.Column(None, db.ForeignKey('user.id'), nullable=True) user = db.relationship(User, backref=db.backref('orders', cascade='all, delete-orphan')) item_collection_id = db.Column(None, db.ForeignKey('item_collection.id'), nullable=False) item_collection = db.relationship('ItemCollection', backref=db.backref( 'orders', cascade='all, delete-orphan', lazy='dynamic')) organization_id = db.Column(None, db.ForeignKey('organization.id'), nullable=False) organization = db.relationship('Organization', backref=db.backref( 'orders', cascade='all, delete-orphan', lazy='dynamic')) status = db.Column(db.Integer, default=ORDER_STATUS.PURCHASE_ORDER, nullable=False) initiated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) paid_at = db.Column(db.DateTime, nullable=True) invoiced_at = db.Column(db.DateTime, nullable=True) cancelled_at = db.Column(db.DateTime, nullable=True) access_token = db.Column(db.Unicode(22), nullable=False, default=buid) buyer_email = db.Column(db.Unicode(254), nullable=False) buyer_fullname = db.Column(db.Unicode(80), nullable=False) buyer_phone = db.Column(db.Unicode(16), nullable=False) invoice_no = db.Column(db.Integer, nullable=True) def confirm_sale(self): """Updates the status to ORDER_STATUS.SALES_ORDER""" for line_item in self.line_items: line_item.confirm() self.invoice_no = get_latest_invoice_no(self.organization) + 1 self.status = ORDER_STATUS.SALES_ORDER self.paid_at = datetime.utcnow() def invoice(self): """Sets invoiced_at, status""" for line_item in self.line_items: line_item.confirm() self.invoiced_at = datetime.utcnow() self.status = ORDER_STATUS.INVOICE def get_amounts(self): """ Calculates and returns the order's base_amount, discounted_amount and final_amount as a namedtuple """ base_amount = Decimal(0) discounted_amount = Decimal(0) final_amount = Decimal(0) for line_item in self.line_items: base_amount += line_item.base_amount discounted_amount += line_item.discounted_amount final_amount += line_item.final_amount return order_amounts_ntuple(base_amount, discounted_amount, final_amount)