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)
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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()
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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)
Exemplo n.º 7
0
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
Exemplo n.º 8
0
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'))
Exemplo n.º 9
0
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')
Exemplo n.º 10
0
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)
Exemplo n.º 11
0
        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)
Exemplo n.º 13
0
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)
Exemplo n.º 14
0
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()
Exemplo n.º 15
0
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
Exemplo n.º 16
0
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)