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)
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)