class Cart(db.Document): meta = { 'db_alias': 'cart_db', 'indexes': ['user_id'] } entries = db.ListField(db.ReferenceField('CartEntry')) logistic_free = db.FloatField() total_price = db.FloatField() user_id = db.StringField() def __repr__(self): return '<Cart: {}>'.format(self.id) @classmethod def get_cart_or_create(cls, user_id): try: cart = cls.objects.get(user_id=user_id) except: cart = cls(user_id=user_id).save() return cart def total_price(self): total_price = 0 for i in self.entries: total_price += i.price self.total_price = total_price return total_price
class Coupon(db.Document): meta = { 'strict': False, 'db_alias': 'order_db', 'indexes': ['code', 'apply', 'expire_date', 'description'] } scope = db.StringField(required=True, choices=COUPON_SCOPE, default=COUPON_SCOPE.ORDER) coupon_type = db.StringField(default=COUPON_TYPES.NORMAL, required=True, choices=COUPON_TYPES) value = db.FloatField() description = db.StringField() effective_date = db.DateTimeField(default=datetime.utcnow) expire_date = db.DateTimeField(default=datetime(2019, 12, 31)) code = db.StringField(unique=True) # by which means this coupon can be applied: by coupon code, by display_id apply = db.StringField(required=True, default=COUPON_APPLY.BY_DISPLAY_ID, choices=COUPON_APPLY) required_amount = db.FloatField(default=0) required_final = db.FloatField(default=0) require_new_order = db.BooleanField(default=False) once_per_user = db.BooleanField(default=False) # note for internal usage note = db.StringField() coupon_category = db.StringField(choices=['PROMOTION', 'STAFF', 'NEW_USER'], default='PROMOTION') @property def is_expired(self): if not self.expire_date: return False return (datetime.utcnow() >= self.expire_date) def to_json(self): return dict( coupon_type=self.coupon_type, value=self.value, code=self.code, effective_date=format_date(self.effective_date, '%Y-%m-%d'), expire_date=format_date(self.expire_date, '%Y-%m-%d'), is_expired=self.is_expired, description=self.description) def is_effective(self): return self.effective_date <= datetime.utcnow() < self.expire_date def can_apply(self, order): res = bool( (self.required_final <= order.final) and (self.required_amount <= order.amount) and not (self.require_new_order and order.customer.orders) and not (self.once_per_user and order.customer.used_coupon(self.code)) ) return res
class BaseEntry(db.Document): meta = {'allow_inheritance': True} spec = db.ReferenceField('ItemSpec') item = db.ReferenceField('Item') amount_usd = db.FloatField(default=0) amount = db.FloatField(default=0) #CNY quantity = db.IntField(default=1, required=True) unit_price = db.FloatField(default=0) # after discount discount = db.ListField(db.DictField()) modified = db.DateTimeField() created_at = db.DateTimeField(default=datetime.utcnow) @property def is_available(self): return (self.spec.availability and self.item.availability) def __unicode__(self): return '%s' % self.id def __repr__(self): # __repr__ method can be used by flask-cache return '{}({}:{})'.format(self.__class__.__name__, self.item_spec_snapshot.sku, self.quantity) def update_amount(self): from application.models import ForexRate self.unit_price = self.item_spec_snapshot.price unit_price_cny = self.unit_price * ForexRate.get() self.amount_usd = self.unit_price * self.quantity self.amount = unit_price_cny * self.quantity self.save() def to_json(self, snapshot=False): item = self.item_snapshot spec = self.item_spec_snapshot item_json = item.to_simple_json() return dict(id=str(self.id), item=item_json, spec=spec.to_json(), unit_price=self.unit_price, amount=self.amount, quantity=self.quantity, weight=item.weight) def clean(self): if self.spec and not self.item: self.item = self.spec.item
class LogisticProvider(db.Document, WeightPrice): meta = { 'db_alias': 'order_db' } name = db.StringField() display_name = db.StringField() description = db.StringField() service_intro = db.DictField() logo = db.StringField() country = db.StringField() is_active = db.BooleanField(default=False) rule_desc = db.StringField() init_price = db.FloatField(required=True) init_weight = db.IntField(required=True) continued_price = db.FloatField(required=True) continued_weight = db.IntField(required=True) init_coin = db.IntField(default=0) features = db.ListField(db.StringField()) promotion = db.StringField(default='') limited_weight = db.IntField(required=True) limited_category = db.ListField(db.StringField()) is_recommended = db.BooleanField(default=False) rating = db.DecimalField(precision=1) rating_users = db.IntField() def __repr__(self): return '<LogisticProvider {}>'.format(self.name) # TODO: memoize here @classmethod def get_provider_shipping(cls, logistic_name, country, weight): if not logistic_name: logistic_name = 'default' provider = cls.objects(name=logistic_name, country=country).first() return provider.get_shipping(weight) @queryset_manager def active(doc_cls, queryset): return queryset.filter(is_active=True) def to_json(self): return dict( name = self.name, display_name = self.display_name, service_intro = self.service_intro, desc = self.description)
class ChannelProvider(db.Document): meta = {'db_alias': 'order_db'} name = db.StringField() display_name = db.StringField() description = db.StringField() service_intro = db.DictField() country = db.StringField() is_active = db.BooleanField(default=False) shipping = db.FloatField(required=True) is_recommended = db.BooleanField(default=False) def __repr__(self): return '<ChannelProvider {}>'.format(self.name) @classmethod def get_shipping(cls, channel_name, country): if not channel_name: channel_name = 'default' provider = cls.objects(name=channel_name, country=country).first() return provider.shipping @queryset_manager def active(doc_cls, queryset): return queryset.filter(is_active=True) def to_json(self): return dict(name=self.name, display_name=self.display_name, service_intro=self.service_intro, desc=self.description)
class EntrySpec(db.Document): meta = { 'db_alias': 'cart_db', 'indexes': ['sku', 'item_id', ] } sku = db.IntField(required=True, unique=True) item_id = db.IntField() title = db.StringField() primary_image = db.StringField() item_available = db.BooleanField() price = db.FloatField() available = db.BooleanField() attributes = db.DictField() images = db.ListField(db.StringField()) attribute_list = db.ListField(db.StringField()) attribute_desc = db.DictField() brand = db.DictField() last_update_date = db.DateTimeField() carts = db.ListField(db.ReferenceField('Cart')) last_empty_date = db.DateTimeField()
class ItemSpec(db.Document): meta = { 'db_alias': 'inventory_db', 'indexes': [ 'item_id', 'web_sku', 'sku', 'price', 'original_price', 'availability', 'attributes', 'created_at', 'stock' ], 'ordering': ['price'] } item_id = db.IntField(required=True) sku = db.SequenceField(required=True, unique=True, primary_key=True) web_sku = db.StringField(required=True) images = db.ListField(db.StringField(required=True)) original_price = db.FloatField(required=True, min_value=0) price = db.FloatField(required=True, min_value=0) china_price = db.FloatField(min_value=0, default=0) availability = db.BooleanField(default=True, required=True) stock = db.IntField(default=-1) # spec.attributes: {color: 'Blue', size: 'M'} attributes = db.DictField() shipping_info = db.DictField() created_at = db.DateTimeField(default=datetime.utcnow, required=True) modified = db.DateTimeField() url = db.StringField() extra = db.DictField(default={}) @property def item(self): return Item.objects(item_id=self.item_id).first() def __unicode__(self): return '%s' % self.sku def update_spec(self, new_spec): for k, v in new_spec.items(): setattr(self, k, v) self.save()
class OrderStat(db.Document): meta = {'db_alias': 'order_db', 'indexes': ['user_id']} user_id = db.ObjectIdField(required=True) orders = db.ListField(db.ReferenceField('Order')) items = db.ListField(db.IntField()) total = db.FloatField(default=0) received = db.FloatField(default=0) num_orders = db.IntField(default=0) num_unpaid = db.IntField(default=0) num_waiting = db.IntField(default=0) def clean(self): for field in ('total', 'received', 'num_orders', 'num_unpaid', 'num_waiting'): if getattr(self, field, 0) < 0: setattr(self, field, 0) @classmethod def by_user(cls, user_id): cls.objects(user_id=user_id).update_one(set__user_id=user_id, upsert=True) return cls.objects(user_id=user_id).first()
class CartEntry(db.Document): meta = { 'db_alias': 'cart_db' } item_id = db.StringField(required=True) title = db.StringField() primary_img = db.StringField() price = db.FloatField() created_at = db.DateTimeField(default=datetime.utcnow) def __unicode__(self): return '%s' % str(self.id) @property def fields(self): return ['item_id', 'title', 'primary_img', 'price', 'created_at'] def to_json(self): result = {f: getattr(self, f) for f in self.fields} return result
class ForexRate(db.Document): currency = db.StringField(default=CURRENCY.USD, choices=CURRENCY) rate = db.FloatField() modified = db.DateTimeField() @classmethod def get(cls, currency=CURRENCY.USD): currency = currency.upper() if currency == 'US': currency = 'USD' if currency == 'CNY': return 1 # rounding datetime, so we can use cache of mongoengine start = (datetime.utcnow() - timedelta(days=3)).replace(minute=0, second=0, microsecond=0) rate_objs = cls.objects(currency=currency, modified__gt=start) rates = [r.rate for r in rate_objs] try: rate = sum(rates) / float(len(rates)) except ZeroDivisionError: rate = 0 if not rate: rate = cls.get_latest_rate(currency) return _ceil(rate + RATE_INCR.get(currency, 0), 3) @classmethod def get_latest_rate(cls, currency=CURRENCY.USD): forex = cls.objects(currency=currency).order_by('-modified').first() rate = forex.rate if forex else DEFAULT_RX_RATE[currency] return rate @classmethod def put(cls, rate, d=None, currency=CURRENCY.USD): if d is None: d = datetime.utcnow() cls.objects(currency=currency, modified=d).update_one(set__rate=rate, upsert=True)
class Price(db.EmbeddedDocument): price = db.FloatField() record_date = db.DateTimeField()
class CartEntry(db.EmbeddedDocument): sku = db.IntField(required=True) quantity = db.IntField(default=1, required=True) created_at = db.DateTimeField() first_price = db.FloatField(default=0)
class Item(db.Document): meta = {'db_alias': 'inventory_db'} owner = db.StringField() owner_id = db.StringField() item_id = db.StringField(primary_key=True) # id eth_token = db.StringField() ## price price = db.FloatField() primary_img = db.StringField() images = db.ListField(db.StringField()) # basic information title = db.StringField() brand = db.StringField() condition = db.StringField() description = db.StringField(default='') madein = db.StringField() #color = db.ListField(db.StringField()) location = db.StringField() main_category = db.StringField() sub_category = db.StringField() sex_tag = db.StringField() tags = db.ListField(db.StringField()) availability = db.BooleanField(default=True) state = db.StringField() # time created_at = db.DateTimeField(default=datetime.utcnow) modified = db.DateTimeField() creator = db.StringField() def __unicode__(self): return '%s' % self.item_id def __repr__(self): return '%s' % self.item_id @property def cart_fields(self): return ['item_id', 'title', 'primary_img', 'price'] def to_cart(self): result = {f: getattr(self, f) for f in self.cart_fields} return result @db.queryset_manager def available_items(doc_cls, queryset): return queryset.filter(availability=True) @property def small_thumbnail(self): return self.primary_img[:23] + 'thumbnails/150x150/' + self.primary_img[ 23:] @property def large_thumbnail(self): return self.primary_img[:23] + 'thumbnails/400x400/' + self.primary_img[ 23:] @classmethod def create(cls, item): item = Item(**item).save() item_id = item.item_id return item_id
class Item(db.Document): meta = {'db_alias': 'inventory_db'} item_id = db.SequenceField(required=True, unique=True, primary_key=True) # id ## price original_price = db.FloatField(required=True, min_value=0) price = db.FloatField(required=True, min_value=0) discount = db.IntField() primary_img = db.StringField(required=True) # basic information vendor = db.StringField(required=True) vendor_id = db.StringField(required=True) brand = db.StringField(required=True) main_category = db.StringField(required=True) sub_category = db.StringField(required=True) sex_tag = db.StringField(required=True, choices=SEX_TAG) tags = db.ListField(db.StringField()) # descritpion title = db.StringField(required=True) title_en = db.StringField() description = db.StringField(default='') attributes = db.ListField(db.StringField()) # 如果有材质的话,以材质开头 information = db.ListField(db.StringField()) size_lookup = db.DictField(default={}) extra = db.DictField(default={}) status = db.StringField(default=ITEM_STATUS.NEW, required=True, choices=ITEM_STATUS) availability = db.BooleanField(default=True, required=True) # time created_at = db.DateTimeField(default=datetime.utcnow, required=True) modified = db.DateTimeField() creator = db.StringField() lookup_table = db.StringField() size_chart = db.ListField(db.StringField()) fields_to_log = { 'url', 'original_price', 'price', 'discount', 'primary_img', 'vendor', 'brand', 'main_category', 'sub_category', 'sex_tag', 'tags', 'availability', } def __unicode__(self): return '%s' % self.item_id def __repr__(self): return '%s' % self.item_id @db.queryset_manager def available_items(doc_cls, queryset): return queryset.filter(availability=True) @property def specs(self): return ItemSpec.objects(item_id=self.item_id) @property def available_specs(self): return ItemSpec.objects(item_id=self.item_id, availability=True) @property def small_thumbnail(self): return self.primary_img[:23] + 'thumbnails/150x150/' + self.primary_img[ 23:] @property def large_thumbnail(self): return self.primary_img[:23] + 'thumbnails/400x400/' + self.primary_img[ 23:] @classmethod def create(cls, item): meta = item['meta'] specs = item.get('specs', []) i = Item.objects(web_id=meta['web_id']) if i: item['meta']['status'] = ITEM_STATUS.MOD return cls.modify(item) # If category, brand and tags does not exisit then create. Category.get_category_or_create(meta['main_category'], 1) Category.get_category_or_create(meta['sub_category'], 2) Brand.get_brand_or_create(meta['brand']) Vendor.get_or_create(meta['vendor']) for tag in meta['tags']: Tag.get_tag_or_create(tag) Statistics.create(meta['main_category'], meta['sub_category'], meta['brand'], meta['tags'], meta['sex_tag']) item = Item(**meta).save() item_id = item.item_id # Keep item specs clean. Remove accidentally left useless specs. old_specs = ItemSpec.objects(item_id=item_id) for spec in old_specs: spec.delete() for spec in specs: spec['item_id'] = item_id ItemSpec(**spec).save() PriceHistory.upsert_price_history(item_id, meta['price']) return item_id @classmethod def modify(cls, new_item, current_price=None): # If the item does not exist in our db, then do not trust the coming # mod data. try: old_item = Item.objects.get(web_id=new_item['meta']['web_id']) except DoesNotExist: current_app.logger.warning( 'crawler send item not exist in db: {}'.format(new_item)) return # Old item is an mongoengine object. # New item is a dictionary. cls.update_item(old_item, new_item) old_item.save() if current_price: PriceHistory.upsert_price_history(old_item.item_id, current_price) return old_item.item_id @classmethod def update_item(cls, old_item, new_item): meta = new_item['meta'] # If category, brand and tags does not exisit then create. Category.get_category_or_create(meta['main_category'], 1) Category.get_category_or_create(meta['sub_category'], 2) Brand.get_brand_or_create(meta['brand']) Vendor.get_or_create(meta['vendor']) Statistics.create(meta['main_category'], meta['sub_category'], meta['brand'], meta['tags'], meta['sex_tag']) for k, v in meta.items(): setattr(old_item, k, v) old_item.save() @classmethod def delete_item(cls, web_id): try: item = cls.objects.get(web_id=web_id) item.status = 'DEL' item.availability = False item.save() return item.item_id except DoesNotExist: pass def to_simple_json(self): return dict(id=str(self.item_id), title=self.title, price=getattr(self, 'price_details', lambda: '')(), primary_image=self.primary_img, status=self.status) def price_details(self): return dict( price=self.price, orig_price=self.original_price, discount=ceil( ((self.original_price - self.price) / self.original_price) * 100), )
class Price(db.EmbeddedDocument): price = db.FloatField(required=True, min_value=0) record_date = db.DateTimeField(default=datetime.utcnow(), required=True)
class Payment(db.Document): meta = { 'db_alias': 'order_db', 'indexes': ['order', 'ptype', '-created_at'] } created_at = db.DateTimeField(default=datetime.datetime.utcnow, required=True) order = db.ReferenceField('Order') logistic = db.ReferenceField('Logistic') other_reason = db.StringField() ptype = db.StringField(required=True, choices=PAYMENT_TYPE) status = db.StringField(max_length=255, required=True, choices=PAYMENT_STATUS, default=PAYMENT_STATUS.UNPAID) # transaction reference number from alipay/bank ref_number = db.StringField(max_length=100) paid_amount = db.FloatField() foreign_amount = db.FloatField() currency = db.StringField() buyer_id = db.StringField(max_length=50) trader = db.StringField(choices=PAYMENT_TRADERS) trade_type = db.StringField(choices=TRADE_TYPE) trade_status = db.StringField() trader_msg = db.StringField() extra = db.StringField() modified = db.DateTimeField() redirect_url = db.StringField() @property def is_paid(self): return self.status == PAYMENT_STATUS.PAID @property def amount(self): if self.ptype == PAYMENT_TYPE.WITHOUT_TAX: return self.order.final if self.ptype == PAYMENT_TYPE.WITH_TAX: return self.order.final + self.order.tax def mark_paid(self, data): if self.is_paid: return self.update(set__status=PAYMENT_STATUS.PAID) kwargs = {'set__' + key: value for key, value in data.items()} self.update(**kwargs) self.reload() paid_amount = float(data.get('paid_amount', 0)) if self.ptype == PAYMENT_TYPE.WITHOUT_TAX: self.order.update_payment(self.ptype, paid_amount, self.trader) def to_json(self): return dict(id=self.id, ref_num=self.ref_number, status=self.status, type=self.type, amount=self.amount)
class Order(db.Document): meta = { 'db_alias': 'order_db', 'ordering': ['-created_at'], 'indexes': [ 'customer_id', 'short_id', 'created_at', 'status', 'address', 'amount', 'final', 'order_type', 'is_paid', 'is_payment_abnormal', 'refund_entries' ] } created_at = db.DateTimeField(default=datetime.datetime.utcnow, required=True) order_type = db.StringField(choices=ORDER_TYPE, default=ORDER_TYPE.COMMODITY) expired_in = db.IntField(default=1440) # in minutes payment_expired_in = db.IntField( default=1440) # minutes before payment should expire short_id = db.SequenceField(required=True, unique=True) is_vip = db.BooleanField(default=False) status = db.StringField(max_length=255, required=True, choices=ORDER_STATUS, default=ORDER_STATUS.PAYMENT_PENDING) status_modified = db.DateTimeField() source = db.StringField(choices=ORDER_SOURCES) is_rewards_given = db.BooleanField(default=False) # order detail amount_usd = db.FloatField(default=0) amount = db.FloatField(default=0) discount = db.ListField(db.DictField()) # only for coupon coupon_codes = db.ListField(db.StringField()) coin = db.IntField() hongbao = db.IntField() cash = db.IntField() final = db.FloatField(required=True) logistic_provider = db.StringField() estimated_tax = db.FloatField(default=0) real_tax = db.FloatField(default=-1) paid_tax = db.FloatField(default=-1) # for internal usage forex = db.FloatField() real_shipping = db.FloatField() # real shipping fee paid cn_shipping = db.FloatField(default=0) address = db.ReferenceField('Address') customer_id = db.ObjectIdField(required=True) is_new_customer = db.BooleanField(default=False) entries = db.ListField( db.ReferenceField('OrderEntry', reverse_delete_rule=mongoengine.PULL)) extra = db.StringField() # keep old property it here for migration logistics = db.ListField(db.ReferenceField('Logistic')) closed_logistics = db.ListField(db.ReferenceField('Logistic')) # this doesn't indicate whether customer have paid tax or not is_paid = db.BooleanField(default=False) is_payment_abnormal = db.BooleanField(default=False) paid_date = db.DateTimeField() pay_tax_deadline = db.DateTimeField() # informations of refundation refund_entries = db.ListField( db.ReferenceField('OrderEntry', reverse_delete_rule=mongoengine.PULL)) refund_amount = db.FloatField(default=0) is_test = db.BooleanField(default=False) fields_to_log = { 'status', 'amount', 'coin', 'final', 'estimated_tax', 'real_tax', 'paid_tax', 'real_shipping', 'is_paid', } PROCESSING_STATUS = [ORDER_STATUS.PAYMENT_RECEIVED, ORDER_STATUS.SHIPPING] ABNORMAL_STATUS = [ ORDER_STATUS.CANCELLED, ORDER_STATUS.ABNORMAL, ORDER_STATUS.ORDER_DELETED, ORDER_STATUS.EXPIRED, ORDER_STATUS.REFUNDED ] def __unicode__(self): return '%s' % self.sid @classmethod def get_order_or_404(cls, order_id, check_user=True): try: order = cls.objects(id=order_id).first_or_404() except mongoengine.ValidationError: try: short_id = int(order_id) except (ValueError, TypeError): abort(404) order = cls.objects(short_id=short_id).first_or_404() if check_user and str(order.customer_id) != str(current_user.id): abort(404) return order @queryset_manager def commodities(doc_cls, queryset): return queryset.filter(order_type=ORDER_TYPE.COMMODITY, status__nin=doc_cls.ABNORMAL_STATUS) @queryset_manager def transfer(doc_cls, queryset): return queryset.filter(order_type=ORDER_TYPE.TRANSFER, status__nin=doc_cls.ABNORMAL_STATUS) @queryset_manager def processing(doc_cls, queryset): return queryset.filter(status__in=doc_cls.PROCESSING_STATUS) @queryset_manager def payment_pending(doc_cls, queryset): return queryset.filter(status=ORDER_STATUS.PAYMENT_PENDING) @queryset_manager def abnormal(doc_cls, queryset): ''' Define abnormal: status is abnormal, or partial entries are refunded.''' return queryset.filter( Q(status__in=doc_cls.ABNORMAL_STATUS) | \ (Q(refund_entries__0__exists=True) & Q(status__in=(doc_cls.PROCESSING_STATUS+[ORDER_STATUS.RECEIVED]))) ) @queryset_manager def received(doc_cls, queryset): return queryset.filter(status=ORDER_STATUS.RECEIVED) def is_processing(self): return self.status in self.PROCESSING_STATUS def is_payment_pending(self): return self.status == ORDER_STATUS.PAYMENT_PENDING def is_abnormal(self): if self.status in self.ABNORMAL_STATUS: return True if self.status in self.PROCESSING_STATUS or self.status == ORDER_STATUS.RECEIVED: return len(self.refund_entries) > 0 return False def has_refund_entries(self): if self.status in (self.PROCESSING_STATUS + [ORDER_STATUS.RECEIVED, ORDER_STATUS.REFUNDED]): return len(self.refund_entries) > 0 return False @property def tax(self): if self.real_tax == -1: return self.estimated_tax else: return self.real_tax @property def shipping(self): return self.cn_shipping @property def estimated_weight(self): return sum( float(entry.item_snapshot.weight) * entry.quantity for entry in self.entries) @property def pay_tax_remain_days(self): if self.pay_tax_deadline: time_remain = self.pay_tax_deadline.date() - datetime.date.today() if time_remain.days > 0: return time_remain.days @property def latest_logistic(self): from application.models import LogisticDetail attr = LogisticDetail.attr_by_log_stat.get(self.status) if not attr: return None return max(self.logistics, key=lambda l: getattr(l.detail, attr)) @classmethod def create_transfer(cls, customer_id, entries, logistic_provider, coupon_codes, coin=0, cash=0, address=None, **kwargs): from application.models import ForexRate order = cls(customer_id=customer_id, entries=entries, logistic_provider=logistic_provider, coupon_codes=coupon_codes, coin=coin, cash=cash, **kwargs) if not order.forex: order.forex = ForexRate.get() order.update_amount() order.reload() if address: order.set_address(address) import application.services.jobs as Jobs #Jobs.stat.update_user_stats(str(order.customer_id)) Signals.order_created.send('system', order=order) return order @classmethod def create_from_skus(cls, customer_id, skus, logistic_provider, coupon_codes, coin=0, cash=0, address=None, **kwargs): from application.models import OrderEntry, ForexRate, Item, ItemSpec entries = [] for e in skus: availability = check_availability_and_update_stock( e['item_id'], e['sku'], e['quantity']) if not availability: return e spec = ItemSpec.objects(sku=e['sku']).first() item = Item.objects(item_id=e['item_id']).first() entry = OrderEntry(spec=spec, item=item, quantity=e['quantity']).save() entries.append(entry) order = cls(customer_id=customer_id, entries=entries, logistic_provider=logistic_provider, coupon_codes=coupon_codes, coin=coin, cash=cash, **kwargs) if not order.forex: order.forex = ForexRate.get() order.update_amount() order.reload() for e in order.entries: e.create_snapshot() if address: order.set_address(address) import application.services.jobs as Jobs #Jobs.stat.update_user_stats(str(order.customer_id)) Signals.order_created.send('system', order=order) return order @classmethod def create(cls, customer_id, entries, logistic_provider, coupon_codes, coin=0, cash=0, address=None, **kwargs): import application.models as Models order_entries = [] for entry in entries: availability = check_availability_and_update_stock( entry.item_snapshot.item_id, entry.item_spec_snapshot.sku, entry.quantity) if not availability: return entry if isinstance(entry, (Models.CartEntry, Models.OrderEntry)): e = deepcopy(entry) e.__class__ = Models.OrderEntry e.id = None order_entries.append(e.save()) # entry.delete() order = cls(customer_id=customer_id, entries=order_entries, logistic_provider=logistic_provider, coupon_codes=coupon_codes, coin=coin, cash=cash, **kwargs) if not order.forex: order.forex = Models.ForexRate.get() order.update_amount() order.reload() for e in order.entries: e.create_snapshot() if address: order.set_address(address) import application.services.jobs as Jobs #Jobs.stat.update_user_stats(str(order.customer_id)) Signals.order_created.send('system', order=order) return order @property def item_changed(self): res = False for e in entries: res = res and e.item_changed if res: return res return res def __get__(self, *args, **kwargs): order = super(Order, self).__get__(*args, **kwargs) if (not order.is_paid) and order.item_changed: order.update_entry() return order def update_entry(self): if self.is_paid: return map(lambda e: e.update_snapshot(), self.entries) self.update_amount() def set_address(self, addr): ''' Create a snapshot of `addr` and make it the address of `self`. It's important to creat a snapshot instead of referencing to the origin address object such that the detail of order's address can not be modified. ''' from application.models import Address if not isinstance(addr, Address): addr = Address.objects(id=addr).first() if not addr: return False addr_snapshot = deepcopy(addr) addr_snapshot.id = None addr_snapshot.order_id = self.id addr_snapshot.save() if self.address: self.address.delete(w=1) self.address = addr_snapshot self.save() return True @property def customer(self): from application import models as Models return Models.User.objects(id=self.customer_id).first() def create_payment(self, ptype, trader): ptype = ptype.upper() self.update_entry() self.reload() if self.order_type != ORDER_TYPE.TRANSFER: is_available = self.check_entries_avaliable() if not is_available or self.status in [ 'CANCELLED', 'ABNORMAL', 'ORDER_DELETED', 'EXPIRED' ]: return None new_payment = Payment(order=self, ptype=ptype, trader=trader).save() return new_payment def get_payment(self, ptype): ptype = ptype.upper() payments = Payment.objects(order=self, ptype=ptype).order_by('-created_at') paid_payments = payments.filter(status=PAYMENT_STATUS.PAID) if paid_payments: return paid_payments.first() else: return payments.first() @property def goods_payment(self): return self.get_payment(PAYMENT_TYPE.WITHOUT_TAX) @property def tax_payment(self): return self.get_payment(PAYMENT_TYPE.WITH_TAX) @property def refunds(self): from application.models import Refund return Refund.objects(order=self) def check_entries_avaliable(self): availability = all( map(lambda e: e.is_available or e.item_spec_snapshot.stock != -1, self.entries)) if not availability: self.status = ORDER_STATUS.EXPIRED self.save() return availability def set_paid(self): if self.is_paid: return self.is_new_customer = not bool( Order.objects(customer_id=self.customer_id, is_paid=True)) self.is_paid = True self.status = ORDER_STATUS.PAYMENT_RECEIVED self.paid_date = datetime.datetime.utcnow() self.save() from application.services.order import payment_received payment_received(self) def update_payment(self, paid_type, paid_amount, trader): if paid_type == PAYMENT_TYPE.WITHOUT_TAX and not self.is_paid and \ self.status in [ORDER_STATUS.PAYMENT_PENDING, ORDER_STATUS.WAREHOUSE_IN]: if paid_amount == self.final and trader == PAYMENT_TRADERS.PAYPAL: self.set_paid() elif paid_amount == float("%.2f" % (self.final * self.forex)) and \ trader == PAYMENT_TRADERS.WEIXIN: self.set_paid() else: current_app.logger.error( 'error at updating payment. trader: {}; ptype: {}; amount:{} order id: {}' .format(trader, paid_type, paid_amount, self.id)) self.is_payment_abnormal = True else: current_app.logger.error( 'error at updating payment. trader: {}; ptype: {}; amount:{} order id: {}' .format(trader, paid_type, paid_amount, self.id)) self.is_payment_abnormal = True self.save() @property def coin_trades(self): import application.models as Models return Models.Trade.objects(reason_id=str(self.id)) def update_logistic_status(self): if self.logistics: log_status = map(lambda m: m.detail.status, self.logistics) new_status = min(log_status, key=lambda l: LOG_STATS.index(l)) self._change_status(new_status) def _change_status(self, new_status): from application.services.noti import noti_order if self.status == new_status: return self.status = new_status self.status_modified = datetime.datetime.utcnow() self.save() if new_status in LOG_STATS: noti_order(self, new_status) Signals.order_logistic_stat_changed.send( 'Order.Logistic.Status.Changed', order=self, new_status=new_status) else: Signals.order_status_changed.send('order_status_changed', order=self, new_status=new_status) def delete_order(self): for mo in self.logistics: mo.delete(w=1) for entry in self.entries: entry.delete(w=1) import application.services.jobs as Jobs #Jobs.stat.update_user_stats(str(self.customer_id)) if self.goods_payment: self.goods_payment.delete(w=1) self.delete(w=1) def cancel_order(self, reason, status=None): for mo in self.logistics: mo.close(reason) self.extra = reason self.save() if not status: status = ORDER_STATUS.ABNORMAL self._change_status(status) for entry in self.entries: try: if entry.spec.stock != -1: entry.spec.update(inc__stock=entry.quantity, set__availability=True) entry.item.update(set__availability=True, set__status='MOD') except AttributeError: pass def update_amount(self): from application.services.price import cal_order_price_and_apply, \ cal_order_tax for e in self.entries: e.update_amount() cal_order_price_and_apply(self) self.estimated_tax = cal_order_tax(self) self.save() @property def sid(self): return self.short_id def to_json(self, include_logistic=False, replace_entries_to_refunded=False): if not self.is_paid: self.update_amount() self.reload() entries_json = [] if replace_entries_to_refunded and self.has_refund_entries(): for e in self.refund_entries: entries_json.append(e.to_json()) else: for e in self.entries: entries_json.append(e.to_json()) refund_entries_json = [] for e in self.refund_entries: refund_entries_json.append(e.to_json()) result = dict( id=str(self.id), short_id=str(self.sid), status=self.status, customer_id=str(self.customer_id), amount=self.amount, cn_shipping=self.cn_shipping, coin=self.coin, hongbao=self.hongbao, discount=self.discount, final=self.final, estimated_tax=self.estimated_tax, payment_status='PAID' if self.is_paid else 'UNPAID', payment_ref_number=[ p.ref_number for p in Payment.objects(order=self) ], created_at=format_date(self.created_at), entries=entries_json, refund_entries=refund_entries_json, refund_amount=self.refund_amount, real_tax=self.real_tax, ) if self.address: result.update({"address": self.address.to_json()}) if include_logistic: result.update(dict(logistics=[l.to_json() for l in self.logistics])) return result def to_grouped_json(self): res = { 'estimated_weight': self.estimated_weight, 'amount': self.amount, 'cn_shipping': self.cn_shipping, 'coin': self.coin, 'hongbao': self.hongbao, 'discount': self.discount, 'final': self.final, 'estimated_tax': self.estimated_tax } res['sid'] = self.sid res['status'] = self.status if self.address: res.update(dict(address=self.address.to_json())) return res
class LogisticDetail(db.EmbeddedDocument): partner_tracking_no = db.StringField(default='', required=True) cn_tracking_no = db.StringField(default='') cn_logistic_name = db.StringField(default='') carrier_tracking_no = db.StringField(default='') partner = db.ReferenceField('Partner') channel = db.StringField() route = db.StringField(default="DEFAULT") pending_review_date = db.DateTimeField() transfer_approved_date = db.DateTimeField() warehouse_in_date = db.DateTimeField() payment_received_date = db.DateTimeField(default=datetime.datetime.utcnow) processing_date = db.DateTimeField() shipping_date = db.DateTimeField() port_arrived_date = db.DateTimeField() received_date = db.DateTimeField() pending_return_date = db.DateTimeField() returned_date = db.DateTimeField() remarks = db.EmbeddedDocumentListField('LogisticRemark') delay_details = db.EmbeddedDocumentListField('LogisticDelay') irregular_details = db.EmbeddedDocumentListField('LogisticIrregular') extra = db.StringField(default='') real_weight = db.FloatField(default=0) real_fee = db.FloatField() modified = db.DateTimeField() modified_by = db.StringField() status = db.StringField(max_length=255, required=True, choices=LOG_STATS, default=LOG_STATS.PAYMENT_RECEIVED) attr_by_log_stat = { 'PENDING_REVIEW': 'pending_review_date', 'TRANSFER_APPROVED': 'transfer_approved_date', 'WAREHOUSE_IN': 'warehouse_in_date', 'PAYMENT_RECEIVED': 'payment_received_date', 'PROCESSING': 'processing_date', 'SHIPPING': 'shipping_date', 'PORT_ARRIVED': 'port_arrived_date', 'RECEIVED': 'received_date', 'PENDING_RETURN': 'pending_return_date', 'RETURNED': 'returned_date' } fields_to_log = { 'cn_tracking_no', 'partner_tracking_no', 'status', 'cn_logistic_name', 'partner', 'payment_received_date', 'shipping_date', 'processing_date', 'received_date', 'port_arrived_date', 'modified_by', } def next(self): if self.status in LOG_STATS and self.status != 'RECEIVED': self.status = LOG_STATS[LOG_STATS.index(self.status) + 1] @property def cn_logistic(self): from application.models import Express return Express.objects(name=self.cn_logistic_name).first()