123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- # -*- coding: utf-8 -*-
- #!/usr/bin/env python
- import datetime
- import logging
- from bson.objectid import ObjectId
- from mongoengine import ObjectIdField, StringField, DateTimeField, ListField, DictField, EmbeddedDocument, IntField, FloatField
- from typing import Dict, Union, AnyStr, Optional, TYPE_CHECKING
- from apilib.monetary import RMB, Percent, Ratio
- from apps.web.constant import PARTITION_ROLE, PARTITION_TYPE
- from apps.web.core.db import Searchable, MonetaryField, PercentField
- from apps.web.core.exceptions import ParameterError
- from apps.web.core.models import LedgerConsumeApp
- from apps.web.device.models import Group
- from apps.web.dealer.define import DEALER_INCOME_SOURCE
- from apps.web.dealer.models import Dealer
- from apps.web.dealer.utils import get_income_source_cls
- if TYPE_CHECKING:
- from apps.web.user.models import RechargeRecord, ConsumeRecord
- from apps.web.ad.models import AdRecord
- from apps.web.device.models import GroupDict
- logger = logging.getLogger(__name__)
- class Partition(EmbeddedDocument):
- role = StringField(verbose_name=u"参与分成的身份", choices=PARTITION_ROLE.choices(), required=True)
- partId = StringField(verbose_name=u"分成ID", name="id")
- money = MonetaryField(verbose_name=u"分得金额")
- shareType = StringField(verbose_name=u"参与分成的方式", choices=PARTITION_TYPE.choices(), default=PARTITION_TYPE.PERCENT)
- # 分成比例仅仅在分成方式为 百分比的时候起作用
- share = PercentField(verbose_name=u"分成比例")
- def to_dict(self):
- data = {
- "id": self.partId,
- "role": self.role,
- "shareType": self.shareType,
- "money": RMB(self.money).mongo_amount,
- }
- if self.shareType == PARTITION_TYPE.PERCENT:
- data["share"] = Percent(self.share).mongo_amount
- return data
- class DealerIncomeProxy(Searchable):
- """
- 资金池的收益代理 相当于不分账 资金全额进入经销商的 deviceBalance
- """
- ref_id = ObjectIdField(verbose_name=u'用来查询所在条目分类模型的主键ID', unique=True)
- source = StringField(verbose_name=u'条目所在在分类')
- title = StringField(verbose_name=u'条目显示标题')
- totalAmount = MonetaryField(verbose_name=u'分账总金额', default = RMB('0.00'))
- dealerIds = ListField(ObjectIdField(verbose_name=u'经销商ID'))
- # 实际的收入划分 仅限于经销商级别 代理商和平台的收益不在这个里面显示
- actualAmountMap = DictField(verbose_name=u'实际分成后的收入,以经销商ID为键')
- groupId = ObjectIdField(verbose_name=u"分账地址")
- logicalCode = StringField(verbose_name=u"设备的逻辑编号")
- # 记录着每一笔的明细分成 包括分成方式 分成的金额 以及分成人的身份和ID
- partition = ListField(verbose_name=u'收入划分明细')
- tags = ListField(verbose_name=u'标签')
- desc = StringField(verbose_name=u'描述')
- # date = StringField(verbose_name = u'日期文本', default = lambda: datetime.datetime.now().strftime(Const.DATE_FMT))
- dateTimeAdded = DateTimeField(verbose_name = u'添加时间', default = datetime.datetime.now)
- _shard_key = ('groupId', 'dateTimeAdded')
- _origin_meta = {
- 'collection': 'dealer_income_proxies',
- 'db_alias': 'report'
- }
- meta = _origin_meta
- search_fields = ('title', 'logicalCode')
- def __repr__(self): return '<DealerIncomeProxy (%s=%s)>' % (self.source, self.ref_id)
- def ref_detail(self, dealerId):
- # type: (str)->dict
- model = get_income_source_cls(self.source) # type: Union[RechargeRecord, AdRecord, ModelProxy]
- partition_dict = {}
- ownerId = None
- for part in self.partition:
- partition_dict[part['id']] = part
- if part['role'] == PARTITION_ROLE.OWNER:
- ownerId = part['id']
- if not ownerId:
- raise Exception('no find owner id partition. income proxy id = {}'.format(str(self.id)))
- from apps.web.common.proxy import ModelProxy
- if issubclass(model, ModelProxy):
- record = model.get_one(shard_filter = {'ownerId': ownerId},
- id = self.ref_id) # type: Union[RechargeRecord, AdRecord]
- rv = record.to_detail()
- else:
- rv = model.objects(id = self.ref_id).get().to_detail()
- # 合伙人可能是后面加入的, 所以在收入MAP里面没有
- if str(dealerId) in self.actualAmountMap:
- rv['amount'] = self.actualAmountMap[str(dealerId)]
- else:
- rv['amount'] = RMB(0)
- rv.update(
- {
- 'incomePartitionList': [
- {
- 'name': Dealer.get_dealer(id_)['nickname'],
- 'amount': RMB(amount),
- 'role': 'me' if id_ == dealerId else 'partner',
- 'owner': partition_dict[id_]['role'] == 'owner', # 加入个字段,标记是否是group owner
- 'percent': partition_dict[id_]['share'],
- 'id': id_
- } for id_, amount in self.actualAmountMap.iteritems()
- ] if len(self.actualAmountMap) > 1 else []
- }
- )
- return rv
- def to_dict(self, dealerId=None):
- # type: (Optional[str])->Dict[str, Union[AnyStr, float]]
- rv = {
- 'id': str(self.id),
- 'title': self.title,
- 'totalAmount': RMB(self.totalAmount),
- 'amount': RMB(0),
- 'createdTime': self.to_datetime_str(self.dateTimeAdded),
- 'source': self.source,
- 'groupId': str(self.groupId)
- }
- if dealerId is None:
- return rv
- else:
- if str(dealerId) in self.actualAmountMap:
- rv['amount'] = RMB(self.actualAmountMap[str(dealerId)])
- return rv
- @classmethod
- def sum_by_dealer(cls, dealerId, **query):
- # type: (ObjectId, dict)->float
- query['dealerIds'] = dealerId
- return cls.objects(**query).sum('actualAmountMap.%s' % (str(dealerId),))
- @staticmethod
- def get_agent_partner_amount(ownerId, partitions):
- agentAmount, partnerAmount = RMB(0.0), RMB(0.0)
- for part in partitions:
- if part['role'] == PARTITION_ROLE.AGENT:
- agentAmount += part['money']
- elif part['id'] != ownerId:
- partnerAmount += part['money']
- return {'agentAmount': agentAmount, 'partnerAmount': partnerAmount}
-
- @staticmethod
- def get_agent_partner_allocated_money(ownerId,partitions,partnerDict):
- agentAmount,partnerAmount,ownerAmount = RMB(0.0),RMB(0.0),RMB(0.0)
- partnerAmountDict = {}
- for part in partitions:
- partId = part['id']
- partMoney = part['money']
- if part['role'] == PARTITION_ROLE.AGENT:
- agentAmount += partMoney
- elif partId == ownerId:
- ownerAmount = partMoney
- else:
- partnerAmount += part['money']
- if partId in partnerDict:
- partnerAmountDict[partId] = {'nickname':partnerDict[partId]['name'],'username':partnerDict[partId]['tel'],'money':partMoney}
- else:
- try:
- partner = Dealer.objects.get(id = partId)
- partnerAmountDict[partId] = {'username':partner.username,'nickname':partner.nickname,'money':partMoney}
- except Exception,e:
- continue
-
- return {'agentAmount':agentAmount,'partnerAmount':partnerAmount,'ownerAmount':ownerAmount,'partnerDict':partnerAmountDict}
- def update_for_refund(self, refund_fee):
- # type:(RMB)->list
- '''
- 按照分账时相同的方向进行退费
- :param refund_fee:
- :return:
- '''
- self.totalAmount = (self.totalAmount - refund_fee)
- income_partion = self.partition
- owner_partion = []
- agent_partion = []
- parter_partion = []
- platform_partion = []
- for item in income_partion:
- if item['role'] == PARTITION_ROLE.OWNER:
- owner_partion.append(item)
- elif item['role'] == PARTITION_ROLE.AGENT:
- agent_partion.append(item)
- elif item['role'] == PARTITION_ROLE.PARTNER:
- parter_partion.append(item)
- elif item['role'] == PARTITION_ROLE.PLATFORM:
- platform_partion.append(item)
- left_refund_fee = refund_fee
- refund_partion = []
- # 扣除的顺序优先是 平台 代理商 合伙人 经销商
- partions = [platform_partion, agent_partion, parter_partion, owner_partion]
- #
- for partion in partions:
- for item in partion:
- my_refund = min(refund_fee * Percent(item['share']).as_ratio, RMB(item['money']),
- left_refund_fee) # type: RMB
- if my_refund > RMB(0):
- refund_partion.append({
- 'role': item['role'],
- 'id': item['id'],
- 'amount': my_refund
- })
- real_money = (RMB(item['money']) - my_refund).mongo_amount
- if item['id'] in self.actualAmountMap:
- self.actualAmountMap[item['id']] = real_money
- item['money'] = real_money
- left_refund_fee = left_refund_fee - my_refund
- if left_refund_fee < RMB(0):
- left_refund_fee = RMB(0)
- self.save()
- return refund_partion
- @property
- def statistic_type(self):
- return "income"
- def get_statistic_update_info(self, amount=None):
- hour = self.dateTimeAdded.hour
- money = RMB(amount or self.totalAmount).mongo_amount
- updateOrInsertData = {
- "add_to_set__origin__{}".format(self.statistic_type): self.id,
- "inc__daily__{}__{}".format(self.statistic_type, self.source): money,
- "inc__hourly__{}__{}__{}".format(hour, self.statistic_type, self.source): money,
- "inc__daily__totalIncome": money,
- "inc__daily__totalIncomeCount": 1
- }
- return updateOrInsertData
- @property
- def partition_map(self):
- partitions = self.partition
- rv = {}
- for role in [PARTITION_ROLE.OWNER, PARTITION_ROLE.AGENT, PARTITION_ROLE.PARTNER]:
- rv[role] = []
- for partition in partitions:
- role = partition['role']
- rv[role].append(partition)
- return rv
- class LedgerInfo(dict):
- _LedgerTime = "time"
- _Partition = "partition"
- _Desc = "desc"
- @property
- def ledgerTime(self):
- return self.get(self._LedgerTime)
- @ledgerTime.setter
- def ledgerTime(self, value):
- self.update({self._LedgerTime: value})
- @property
- def partition(self):
- return self.get(self._Partition)
- @partition.setter
- def partition(self, value):
- self.update({self._Partition: value})
- @property
- def desc(self):
- return self.get(self._Desc)
- @desc.setter
- def desc(self, value):
- self.update({self._Desc: value})
- class DealerGroupStats(Searchable):
- """
- 经销商的每日的地址收益的统计值
- """
- date = StringField(verbose_name="日期 单位天", required=True)
- dealerId = StringField(verbose_name="经销商ID", required=True)
- groupId = StringField(verbose_name="地址ID", required=True)
- orderCount = IntField(verbose_name=u"累计订单数量")
- elecCount = FloatField(verbose_name=u"累计耗电量")
- amount = MonetaryField(verbose_name=u"累计收益")
- bestowAmount = MonetaryField(verbose_name=u"累计消费赠送金额")
- duration = IntField(verbose_name=u"累计充电时长")
- withdrawSourceKey = StringField(verbose_name=u"提现网关")
- ledgerInfo = DictField()
- dateTimeAdded = DateTimeField(verbose_name=u"添加时间", default=datetime.datetime.now)
- """
- 对于订单的记录 最好是维护一个队列 在队列里面进行处理
- """
- def __str__(self):
- return "{}-{}".format(self.__class__, str(self.id))
- @classmethod
- def update_group_stats(cls, group, order, date=None): # type:(GroupDict, ConsumeRecord, datetime.datetime) -> Optional[DealerGroupStats, None]
- """
- 更新当日的数据 如果更新成功 即返回记录供order建立引用关系
- 如果建立失败
- """
- dealer = order.owner
- app = LedgerConsumeApp.get_app(dealer)
- gateway = app.new_gateway("")
- date = (date or datetime.datetime.today()).strftime("%Y-%m-%d")
- result = cls.objects.filter(
- date=date,
- groupId=group.groupId,
- dealerId=group.ownerId
- ).update_one(
- upsert=True,
- inc__orderCount=1,
- inc__elecCount=order.service.elec,
- inc__amount=order.actualAmount,
- inc__bestowAmount=order.bestowAmount,
- inc__duration=order.service.duration,
- withdrawSourceKey=gateway.withdraw_source_key()
- )
- if result:
- return cls.objects.get(date=date, groupId=group.groupId)
- else:
- return None
- @property
- def ledger(self): # type:() -> LedgerInfo
- return LedgerInfo(self.ledgerInfo)
- @property
- def is_ledgered(self):
- return bool(self.ledger.ledgerTime)
- @property
- def ledger_enable(self):
- return datetime.date.today() > datetime.datetime.strptime(self.date, "%Y-%m-%d").date()
- @property
- def group(self): # type:()-> GroupDict
- return Group.get_group(self.groupId)
- @property
- def dealer(self):
- return Dealer.objects.filter(id=self.dealerId).first()
- def set_ledgered(self, time):
- """
- 设置
- """
- if not self.ledger:
- return
- ledgerInfo = self.ledger
- ledgerInfo.ledgerTime = time
- self.ledgerInfo = ledgerInfo
- return self.save()
- def set_partition(self, partition):
- ledgerInfo = self.ledger
- ledgerInfo.partition = partition
- self.ledgerInfo = ledgerInfo
- return self.save()
- def set_description(self, desc):
- ledgerInfo = self.ledger
- ledgerInfo.desc = desc
- self.ledgerInfo = ledgerInfo
- return self.save()
- def record_income_proxy(source, record, partitionMap=None, dateTime=None):
- # type: (str, RechargeRecord, dict, datetime.datetime)->Optional[DealerIncomeProxy]
- """
- 记录收益代理,会记录分成的情况,需要验证比例是合法的, 包括代理商分成经销商的情况
- e.g [{'role': 'agent', 'percent': 80, 'id': ''}, {'role': 'owner', 'percent': 20, 'id': ''}]
- 代理需要记录参与收益的经销商ID列表
- :param source:
- :param record:
- :param partitionMap: 以前是分账列表 目前项目不需要这个参数 用户充值的金额全额进入经销商的资金账户
- :param dateTime:
- :return:
- """
- try:
- if not dateTime:
- if not record.finishedTime:
- dateTime = datetime.datetime.now()
- else:
- dateTime = record.finishedTime
- logger.debug('record_income_proxy source=%s record=%r partitionMap=%s dateTime=%s' % (source, record, partitionMap, dateTime))
- validIncomeSources = DEALER_INCOME_SOURCE.choices()
- if source not in validIncomeSources:
- raise ParameterError('invalid source, only %s are supported, %s given' % (validIncomeSources, source))
- # 将map的结构转换为list的结构
- partition = [{
- "role": "owner",
- "money": record.mongo_amount,
- "id": record.ownerId,
- "share": Ratio(100).mongo_amount
- }]
- def get_dealer_money_map():
- return {
- record.ownerId: record.mongo_amount
- }
- # : 存储代理
- proxy = DealerIncomeProxy(
- ref_id=record.id,
- dealerIds=[ObjectId(record.ownerId)],
- partition=partition,
- groupId=ObjectId(record.groupId),
- logicalCode=record.logicalCode,
- title=record.subject,
- source=source,
- totalAmount=record.mongo_amount,
- actualAmountMap=get_dealer_money_map(),
- dateTimeAdded=dateTime)
- proxy.save()
- return proxy
- except Exception as e:
- logger.exception('cannot record dealers income, error=%s, record=%s' % (e, record))
- raise e
|