示例#1
0
class ShippingMethod(Model):
    __tablename__ = "checkout_shippingmethod"
    title = Column(db.String(255), nullable=False)
    price = Column(db.DECIMAL(10, 2))

    def __str__(self):
        return self.title + "   $" + str(self.price)
示例#2
0
class OrderPayment(Model):
    __tablename__ = "order_payment"
    order_id = Column(db.Integer())
    status = Column(TINYINT)
    total = Column(db.DECIMAL(10, 2))
    delivery = Column(db.DECIMAL(10, 2))
    description = Column(db.Text())
    customer_ip_address = Column(db.String(100))
    token = Column(db.String(100))
    payment_method = Column(db.String(255))
    payment_no = Column(db.String(255), unique=True)
    paid_at = Column(db.DateTime())

    def pay_success(self, paid_at):
        self.paid_at = paid_at
        self.status = PaymentStatusKinds.confirmed.value
        order = Order.get_by_id(self.order_id)
        order.pay_success(payment=self)
示例#3
0
class OrderPayment(Model):
    __tablename__ = "order_payment"
    order_id = Column(db.Integer())
    status = Column(db.Integer)
    total = Column(db.DECIMAL(10, 2))
    delivery = Column(db.DECIMAL(10, 2))
    description = Column(db.Text())
    customer_ip_address = Column(db.String(100))
    token = Column(db.String(100))
    payment_method = Column(db.String(255))
    payment_no = Column(db.String(255), unique=True)
    paid_at = Column(db.DateTime())

    def pay_success(self, paid_at):
        self.paid_at = paid_at
        self.status = PaymentStatusKinds.confirmed.value
        self.save(commit=False)
        order = Order.get_by_id(self.order_id)
        order.pay_success(payment=self)

    @property
    def status_human(self):
        return PaymentStatusKinds(int(self.status)).name
示例#4
0
class OrderLine(Model):
    __tablename__ = "order_line"
    product_name = Column(db.String(255))
    product_sku = Column(db.String(100))
    quantity = Column(db.Integer())
    unit_price_net = Column(db.DECIMAL(10, 2))
    is_shipping_required = Column(db.Boolean(), default=True)
    order_id = Column(db.Integer())
    variant_id = Column(db.Integer())

    @property
    def variant(self):
        return ProductVariant.get_by_id(self.variant_id)

    def get_total(self):
        return self.unit_price_net * self.quantity
示例#5
0
class Order(Model):
    __tablename__ = "order_order"
    token = Column(db.String(100), unique=True)
    shipping_address = Column(db.String(255))
    user_id = Column(db.Integer())
    total_net = Column(db.DECIMAL(10, 2))
    discount_amount = Column(db.DECIMAL(10, 2), default=0)
    discount_name = Column(db.String(100))
    voucher_id = Column(db.Integer())
    shipping_price_net = Column(db.DECIMAL(10, 2))
    status = Column(TINYINT())
    shipping_method_name = Column(db.String(100))
    shipping_method_id = Column(db.Integer())
    ship_status = Column(TINYINT())

    def __str__(self):
        return f"#{self.identity}"

    @classmethod
    def create_whole_order(cls, cart, note=None):
        # Step1, certify stock, voucher
        to_update_variants = []
        to_update_orderlines = []
        total_net = 0
        for line in cart.lines:
            variant = ProductVariant.get_by_id(line.variant.id)
            result, msg = variant.check_enough_stock(line.quantity)
            if result is False:
                return result, msg
            variant.quantity_allocated += line.quantity
            to_update_variants.append(variant)
            orderline = OrderLine(
                variant_id=variant.id,
                quantity=line.quantity,
                product_name=variant.display_product(),
                product_sku=variant.sku,
                product_id=variant.sku.split("-")[0],
                unit_price_net=variant.price,
                is_shipping_required=variant.is_shipping_required,
            )
            to_update_orderlines.append(orderline)
            total_net += orderline.get_total()

        voucher = None
        if cart.voucher_code:
            voucher = Voucher.get_by_code(cart.voucher_code)
            try:
                voucher.check_available(cart)
            except Exception as e:
                return False, str(e)

        # Step2, create Order obj
        try:
            shipping_method_id = None
            shipping_method_title = None
            shipping_method_price = 0
            shipping_address = None

            if cart.shipping_method_id:
                shipping_method = ShippingMethod.get_by_id(
                    cart.shipping_method_id)
                shipping_method_id = shipping_method.id
                shipping_method_title = shipping_method.title
                shipping_method_price = shipping_method.price
                shipping_address = UserAddress.get_by_id(
                    cart.shipping_address_id).full_address

            order = cls.create(
                user_id=current_user.id,
                token=str(uuid4()),
                shipping_method_id=shipping_method_id,
                shipping_method_name=shipping_method_title,
                shipping_price_net=shipping_method_price,
                shipping_address=shipping_address,
                status=OrderStatusKinds.unfulfilled.value,
                total_net=total_net,
            )
        except Exception as e:
            return False, str(e)

        # Step3, process others
        if note:
            order_note = OrderNote(order_id=order.id,
                                   user_id=current_user.id,
                                   content=note)
            db.session.add(order_note)
        if voucher:
            order.voucher_id = voucher.id
            order.discount_amount = voucher.get_vouchered_price(cart)
            order.discount_name = voucher.title
            voucher.used += 1
            db.session.add(order)
            db.session.add(voucher)
        for variant in to_update_variants:
            db.session.add(variant)
        for orderline in to_update_orderlines:
            orderline.order_id = order.id
            db.session.add(orderline)
        for line in cart.lines:
            db.session.delete(line)
        db.session.delete(cart)

        db.session.commit()
        return order, "success"

    def get_absolute_url(self):
        return url_for("order.show", token=self.token)

    @property
    def identity(self):
        return self.token.split("-")[-1]

    @property
    def total(self):
        return self.total_net + self.shipping_price_net - self.discount_amount

    @property
    def status_human(self):
        return OrderStatusKinds(int(self.status)).name

    @property
    def total_human(self):
        return "$" + str(self.total)

    @classmethod
    def get_current_user_orders(cls):
        if current_user.is_authenticated:
            orders = (cls.query.filter_by(user_id=current_user.id).order_by(
                Order.id.desc()).all())
        else:
            orders = []
        return orders

    @classmethod
    def get_user_orders(cls, user_id):
        return cls.query.filter_by(user_id=user_id).all()

    @property
    def is_shipping_required(self):
        return any(line.is_shipping_required for line in self.lines)

    @property
    def is_self_order(self):
        return self.user_id == current_user.id

    @property
    def lines(self):
        return OrderLine.query.filter(OrderLine.order_id == self.id).all()

    @property
    def notes(self):
        return OrderNote.query.filter(OrderNote.order_id == self.id).all()

    @property
    def user(self):
        return User.get_by_id(self.user_id)

    @property
    def payment(self):
        return OrderPayment.query.filter_by(order_id=self.id).first()

    def pay_success(self, payment):
        self.status = OrderStatusKinds.fulfilled.value
        # to resolve another instance with key is already present in this session
        local_obj = db.session.merge(self)
        db.session.add(local_obj)

        for line in self.lines:
            variant = line.variant
            variant.quantity_allocated -= line.quantity
            variant.quantity -= line.quantity
            db.session.add(variant)

        db.session.commit()

        OrderEvent.create(
            order_id=self.id,
            user_id=self.user_id,
            type_=OrderEvents.payment_captured.value,
        )

    def cancel(self):
        self.status = OrderStatusKinds.canceled.value
        db.session.add(self)

        for line in self.lines:
            variant = line.variant
            variant.quantity_allocated -= line.quantity
            db.session.add(variant)

        db.session.commit()

        OrderEvent.create(
            order_id=self.id,
            user_id=self.user_id,
            type_=OrderEvents.order_canceled.value,
        )

    def complete(self):
        self.update(status=OrderStatusKinds.completed.value)
        OrderEvent.create(
            order_id=self.id,
            user_id=self.user_id,
            type_=OrderEvents.order_completed.value,
        )

    def draft(self):
        self.update(status=OrderStatusKinds.draft.value)
        OrderEvent.create(
            order_id=self.id,
            user_id=self.user_id,
            type_=OrderEvents.draft_created.value,
        )

    def delivered(self):
        self.update(
            status=OrderStatusKinds.shipped.value,
            ship_status=ShipStatusKinds.delivered.value,
        )
        OrderEvent.create(
            order_id=self.id,
            user_id=self.user_id,
            type_=OrderEvents.order_delivered.value,
        )
示例#6
0
class Voucher(Model):
    __tablename__ = "discount_voucher"
    type_ = Column("type", TINYINT())
    title = Column(db.String(255))
    code = Column(db.String(16), unique=True)
    usage_limit = Column(db.Integer())
    used = Column(db.Integer(), default=0)
    start_date = Column(db.Date())
    end_date = Column(db.Date())
    discount_value_type = Column(TINYINT())
    discount_value = Column(db.DECIMAL(10, 2))
    limit = Column(db.DECIMAL(10, 2))
    category_id = Column(db.Integer())
    product_id = Column(db.Integer())

    def __str__(self):
        return self.title

    @property
    def type_human(self):
        return VoucherTypeKinds(int(self.type_)).name

    @property
    def discount_value_type_human(self):
        return DiscountValueTypeKinds(int(self.discount_value_type)).name

    @property
    def validity_period(self):
        if self.start_date and self.end_date:
            return (datetime.strftime(self.start_date, "%m/%d/%Y") + " - " +
                    datetime.strftime(self.end_date, "%m/%d/%Y"))
        return ""

    @classmethod
    def generate_code(cls):
        code = "".join(random.choices(string.ascii_uppercase, k=16))
        exist = cls.query.filter_by(code=code).first()
        if not exist:
            return code
        else:
            return cls.generate_code()

    def check_available(self, cart=None):
        if self.start_date and self.start_date > datetime.now():
            raise Exception(
                "The voucher code can not use now, please retry later")
        if self.end_date and self.end_date < datetime.now():
            raise Exception("The voucher code has expired")
        if self.usage_limit and self.usage_limit - self.used < 0:
            raise Exception("This voucher code has been used out")
        if cart:
            self.check_available_by_cart(cart)

        return True

    def check_available_by_cart(self, cart):
        if self.type == VoucherTypeKinds.value.value:
            if self.limit and cart.subtotal < self.limit:
                raise Exception(
                    f"The order total amount is not enough({self.limit}) to use this voucher code"
                )
        elif self.type == VoucherTypeKinds.shipping.value:
            if self.limit and cart.shipping_method_price < self.limit:
                raise Exception(
                    f"The order shipping price is not enough({self.limit}) to use this voucher code"
                )
        elif self.type == VoucherTypeKinds.product.value:
            product = Product.get_by_id(self.product_id)
            # got any product in cart, should be zero
            if cart.get_product_price(self.product_id) == 0:
                raise Exception(
                    f"This Voucher Code should be used for {product.title}")
            if self.limit and cart.get_product_price(
                    self.product_id) < self.limit:
                raise Exception(
                    f"The product {product.title} total amount is not enough({self.limit}) to use this voucher code"
                )
        elif self.type == VoucherTypeKinds.category.value:
            category = Category.get_by_id(self.category_id)
            if cart.get_category_price(self.category_id) == 0:
                raise Exception(
                    f"This Voucher Code should be used for {category.title}")
            if self.limit and cart.get_category_price(
                    self.category_id) < self.limit:
                raise Exception(
                    f"The category {category.title} total amount is not enough({self.limit}) to use this voucher code"
                )

    @classmethod
    def get_by_code(cls, code):
        return cls.query.filter_by(code=code).first()

    def get_vouchered_price(self, cart):
        if self.type == VoucherTypeKinds.value.value:
            return self.get_voucher_from_price(cart.subtotal)
        elif self.type == VoucherTypeKinds.shipping.value:
            return self.get_voucher_from_price(cart.shipping_method_price)
        elif self.type == VoucherTypeKinds.product.value:
            return self.get_voucher_from_price(
                cart.get_product_price(self.product_id))
        elif self.type == VoucherTypeKinds.category.value:
            return self.get_voucher_from_price(
                cart.get_category_price(self.category_id))
        return 0

    def get_voucher_from_price(self, price):
        if self.discount_value_type == DiscountValueTypeKinds.fixed.value:
            return self.discount_value if price > self.discount_value else price
        elif self.discount_value_type == DiscountValueTypeKinds.percent.value:
            price = price * self.discount_value / 100
            return Decimal(price).quantize(Decimal("0.00"))
示例#7
0
class Sale(Model):
    __tablename__ = "discount_sale"
    discount_value_type = Column(TINYINT())
    title = Column(db.String(255))
    discount_value = Column(db.DECIMAL(10, 2))

    def __str__(self):
        return self.title

    @property
    def discount_value_type_label(self):
        return DiscountValueTypeKinds(int(self.discount_value_type)).name

    @classmethod
    def get_discounted_price(cls, product):
        sale_product = SaleProduct.query.filter_by(
            product_id=product.id).first()
        if sale_product:
            sale = Sale.get_by_id(sale_product.sale_id)
        else:
            sale_category = SaleCategory.query.filter_by(
                category_id=product.category.id).first()
            sale = Sale.get_by_id(
                sale_category.sale_id) if sale_category else None
        if sale is None:
            return 0
        if sale.discount_value_type == DiscountValueTypeKinds.fixed.value:
            return sale.discount_value
        elif sale.discount_value_type == DiscountValueTypeKinds.percent.value:
            price = product.basic_price * sale.discount_value / 100
            return Decimal(price).quantize(Decimal("0.00"))

    @property
    def categories(self):
        at_ids = (SaleCategory.query.with_entities(
            SaleCategory.category_id).filter(
                SaleCategory.sale_id == self.id).all())
        return Category.query.filter(Category.id.in_(id
                                                     for id, in at_ids)).all()

    @property
    def products_ids(self):
        return (SaleProduct.query.with_entities(SaleProduct.product_id).filter(
            SaleProduct.sale_id == self.id).all())

    @property
    def products(self):
        return Product.query.filter(
            Product.id.in_(id for id, in self.products_ids)).all()

    def update_categories(self, category_ids):
        origin_ids = (SaleCategory.query.with_entities(
            SaleCategory.category_id).filter_by(sale_id=self.id).all())
        origin_ids = set(i for i, in origin_ids)
        new_attrs = set(int(i) for i in category_ids)
        need_del = origin_ids - new_attrs
        need_add = new_attrs - origin_ids
        for id in need_del:
            SaleCategory.query.filter_by(
                sale_id=self.id, category_id=id).first().delete(commit=False)
        for id in need_add:
            new = SaleCategory(sale_id=self.id, category_id=id)
            db.session.add(new)
        db.session.commit()

    def update_products(self, product_ids):
        origin_ids = (SaleProduct.query.with_entities(
            SaleProduct.product_id).filter_by(sale_id=self.id).all())
        origin_ids = set(i for i, in origin_ids)
        new_attrs = set(int(i) for i in product_ids)
        need_del = origin_ids - new_attrs
        need_add = new_attrs - origin_ids
        for id in need_del:
            SaleProduct.query.filter_by(
                sale_id=self.id, product_id=id).first().delete(commit=False)
        for id in need_add:
            new = SaleProduct(sale_id=self.id, product_id=id)
            db.session.add(new)
        db.session.commit()

    @staticmethod
    def clear_mc(target):
        # when update sales, need to update product discounts
        # for (id,) in target.products_ids:
        #     rdb.delete(MC_KEY_PRODUCT_DISCOUNT_PRICE.format(id))

        # need to process so many states, category update etc.. so delete all
        keys = rdb.keys(MC_KEY_PRODUCT_DISCOUNT_PRICE.format("*"))
        for key in keys:
            rdb.delete(key)

    @classmethod
    def __flush_insert_event__(cls, target):
        super().__flush_insert_event__(target)
        target.clear_mc(target)

    @classmethod
    def __flush_after_update_event__(cls, target):
        super().__flush_after_update_event__(target)
        target.clear_mc(target)

    @classmethod
    def __flush_delete_event__(cls, target):
        super().__flush_delete_event__(target)
        target.clear_mc(target)
示例#8
0
class ProductVariant(Model):
    __tablename__ = "product_variant"
    sku = Column(db.String(32), unique=True)
    title = Column(db.String(255))
    price_override = Column(db.DECIMAL(10, 2), default=0.00)
    quantity = Column(db.Integer(), default=0)
    quantity_allocated = Column(db.Integer(), default=0)
    product_id = Column(db.Integer(), default=0)
    attributes = Column(MutableDict.as_mutable(db.JSON()))

    def __str__(self):
        return self.title or self.sku

    def display_product(self):
        return f"{self.product} ({str(self)})"

    @property
    def sku_id(self):
        return self.sku.split("-")[1]

    @sku_id.setter
    def sku_id(self, data):
        pass

    @property
    def is_shipping_required(self):
        return self.product.product_type.is_shipping_required

    @property
    def quantity_available(self):
        return max(self.quantity - self.quantity_allocated, 0)

    @property
    def is_in_stock(self):
        return self.quantity_available > 0

    @property
    def stock(self):
        return self.quantity - self.quantity_allocated

    @property
    def price(self):
        return self.price_override or self.product.price

    @property
    def product(self):
        return Product.get_by_id(self.product_id)

    def get_absolute_url(self):
        return url_for("product.show", id=self.product.id)

    @property
    def attribute_map(self):
        items = {
            ProductAttribute.get_by_id(k): AttributeChoiceValue.get_by_id(v)
            for k, v in self.attributes.items()
        }
        return items

    def check_enough_stock(self, quantity):
        if self.stock < quantity:
            return False, f"{self.display_product()} has not enough stock"
        return True, "success"

    @staticmethod
    def clear_mc(target):
        rdb.delete(MC_KEY_PRODUCT_VARIANT.format(target.product_id))

    @classmethod
    def __flush_insert_event__(cls, target):
        super().__flush_insert_event__(target)
        target.clear_mc(target)

    @classmethod
    def __flush_after_update_event__(cls, target):
        super().__flush_after_update_event__(target)
        target.clear_mc(target)

    @classmethod
    def __flush_delete_event__(cls, target):
        super().__flush_delete_event__(target)
        target.clear_mc(target)
示例#9
0
class Product(Model):
    __tablename__ = "product_product"
    title = Column(db.String(255), nullable=False)
    on_sale = Column(db.Boolean(), default=True)
    rating = Column(db.DECIMAL(8, 2), default=5.0)
    sold_count = Column(db.Integer(), default=0)
    review_count = Column(db.Integer(), default=0)
    basic_price = Column(db.DECIMAL(10, 2))
    category_id = Column(db.Integer())
    is_featured = Column(db.Boolean(), default=False)
    product_type_id = Column(db.Integer())
    attributes = Column(MutableDict.as_mutable(db.JSON()))
    description = Column(db.Text())
    if Config.USE_REDIS:
        description = PropsItem("description")

    def __str__(self):
        return self.title

    def __iter__(self):
        return iter(self.variants)

    def get_absolute_url(self):
        return url_for("product.show", id=self.id)

    @property
    @cache(MC_KEY_PRODUCT_IMAGES.format("{self.id}"))
    def images(self):
        return ProductImage.query.filter(
            ProductImage.product_id == self.id).all()

    @property
    def first_img(self):
        if self.images:
            return str(self.images[0])
        return ""

    @property
    def is_in_stock(self):
        return any(variant.is_in_stock for variant in self)

    @property
    def category(self):
        return Category.get_by_id(self.category_id)

    @property
    def product_type(self):
        return ProductType.get_by_id(self.product_type_id)

    @property
    def is_discounted(self):
        if float(self.discounted_price) > 0:
            return True
        return False

    @property
    @cache(MC_KEY_PRODUCT_DISCOUNT_PRICE.format("{self.id}"))
    def discounted_price(self):
        from flaskshop.discount.models import Sale

        return Sale.get_discounted_price(self)

    @property
    def price(self):
        if self.is_discounted:
            return self.basic_price - self.discounted_price
        return self.basic_price

    @property
    def price_human(self):
        return "$" + str(self.price)

    @property
    def on_sale_human(self):
        return "Y" if self.on_sale else "N"

    @property
    @cache(MC_KEY_PRODUCT_VARIANT.format("{self.id}"))
    def variant(self):
        return ProductVariant.query.filter(
            ProductVariant.product_id == self.id).all()

    @property
    def attribute_map(self):
        items = {
            ProductAttribute.get_by_id(k): AttributeChoiceValue.get_by_id(v)
            for k, v in self.attributes.items()
        }
        return items

    @classmethod
    # @cache(MC_KEY_FEATURED_PRODUCTS.format("{num}"))
    def get_featured_product(cls, num=8):  # 首頁的 featured products
        return cls.query.filter_by(is_featured=True).limit(num).all()

    def update_images(self, new_images):
        origin_ids = (ProductImage.query.with_entities(
            ProductImage.product_id).filter_by(product_id=self.id).all())
        origin_ids = set(i for i, in origin_ids)
        new_images = set(int(i) for i in new_images)
        need_del = origin_ids - new_images
        need_add = new_images - origin_ids
        for id in need_del:
            ProductImage.get_by_id(id).delete(commit=False)
        for id in need_add:
            image = ProductImage.get_by_id(id)
            image.product_id = self.id
            image.save(commit=False)
        db.session.commit()

    def update_attributes(self, attr_values):
        attr_entries = [
            str(item.id) for item in self.product_type.product_attributes
        ]
        attributes = dict(zip(attr_entries, attr_values))
        self.attributes = attributes

    def generate_variants(self):
        if not self.product_type.has_variants:
            ProductVariant.create(sku=str(self.id) + "-1337",
                                  product_id=self.id)
        else:
            sku_id = 1337
            variant_attributes = self.product_type.variant_attributes[0]
            for value in variant_attributes.values:
                sku = str(self.id) + "-" + str(sku_id)
                attributes = {str(variant_attributes.id): str(value.id)}
                ProductVariant.create(
                    sku=sku,
                    title=value.title,
                    product_id=self.id,
                    attributes=attributes,
                )
                sku_id += 1

    def delete(self):
        need_del_collection_products = ProductCollection.query.filter_by(
            product_id=self.id).all()
        for item in itertools.chain(self.images, self.variant,
                                    need_del_collection_products):
            item.delete(commit=False)
        db.session.delete(self)
        db.session.commit()

    @staticmethod
    def clear_mc(target):
        rdb.delete(MC_KEY_PRODUCT_DISCOUNT_PRICE.format(target.id))
        keys = rdb.keys(MC_KEY_FEATURED_PRODUCTS.format("*"))
        for key in keys:
            rdb.delete(key)

    @staticmethod
    def clear_category_cache(target):
        keys = rdb.keys(
            MC_KEY_CATEGORY_PRODUCTS.format(target.category_id, "*"))
        for key in keys:
            rdb.delete(key)

    @classmethod
    def __flush_insert_event__(cls, target):
        super().__flush_insert_event__(target)

        if current_app.config["USE_ES"]:
            from flaskshop.public.search import Item

            Item.add(target)

    @classmethod
    def __flush_before_update_event__(cls, target):

        super().__flush_before_update_event__(target)
        target.clear_category_cache(target)

    @classmethod
    def __flush_after_update_event__(cls, target):

        super().__flush_after_update_event__(target)
        target.clear_mc(target)
        target.clear_category_cache(target)
        if current_app.config["USE_ES"]:
            from flaskshop.public.search import Item

            Item.update_item(target)

    @classmethod
    def __flush_delete_event__(cls, target):
        from flaskshop.public.search import Item

        super().__flush_delete_event__(target)
        target.clear_mc(target)
        target.clear_category_cache(target)
        Item.delete(target)