# -*- 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 '' % (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