proxy.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. # -*- coding: utf-8 -*-
  2. #!/usr/bin/env python
  3. import datetime
  4. import logging
  5. from bson.objectid import ObjectId
  6. from mongoengine import ObjectIdField, StringField, DateTimeField, ListField, DictField, EmbeddedDocument, IntField, FloatField
  7. from typing import Dict, Union, AnyStr, Optional, TYPE_CHECKING
  8. from apilib.monetary import RMB, Percent, Ratio
  9. from apps.web.constant import PARTITION_ROLE, PARTITION_TYPE
  10. from apps.web.core.db import Searchable, MonetaryField, PercentField
  11. from apps.web.core.exceptions import ParameterError
  12. from apps.web.core.models import LedgerConsumeApp
  13. from apps.web.device.models import Group
  14. from apps.web.dealer.define import DEALER_INCOME_SOURCE
  15. from apps.web.dealer.models import Dealer
  16. from apps.web.dealer.utils import get_income_source_cls
  17. if TYPE_CHECKING:
  18. from apps.web.user.models import RechargeRecord, ConsumeRecord
  19. from apps.web.ad.models import AdRecord
  20. from apps.web.device.models import GroupDict
  21. logger = logging.getLogger(__name__)
  22. class Partition(EmbeddedDocument):
  23. role = StringField(verbose_name=u"参与分成的身份", choices=PARTITION_ROLE.choices(), required=True)
  24. partId = StringField(verbose_name=u"分成ID", name="id")
  25. money = MonetaryField(verbose_name=u"分得金额")
  26. shareType = StringField(verbose_name=u"参与分成的方式", choices=PARTITION_TYPE.choices(), default=PARTITION_TYPE.PERCENT)
  27. # 分成比例仅仅在分成方式为 百分比的时候起作用
  28. share = PercentField(verbose_name=u"分成比例")
  29. def to_dict(self):
  30. data = {
  31. "id": self.partId,
  32. "role": self.role,
  33. "shareType": self.shareType,
  34. "money": RMB(self.money).mongo_amount,
  35. }
  36. if self.shareType == PARTITION_TYPE.PERCENT:
  37. data["share"] = Percent(self.share).mongo_amount
  38. return data
  39. class DealerIncomeProxy(Searchable):
  40. """
  41. 资金池的收益代理 相当于不分账 资金全额进入经销商的 deviceBalance
  42. """
  43. ref_id = ObjectIdField(verbose_name=u'用来查询所在条目分类模型的主键ID', unique=True)
  44. source = StringField(verbose_name=u'条目所在在分类')
  45. title = StringField(verbose_name=u'条目显示标题')
  46. totalAmount = MonetaryField(verbose_name=u'分账总金额', default = RMB('0.00'))
  47. dealerIds = ListField(ObjectIdField(verbose_name=u'经销商ID'))
  48. # 实际的收入划分 仅限于经销商级别 代理商和平台的收益不在这个里面显示
  49. actualAmountMap = DictField(verbose_name=u'实际分成后的收入,以经销商ID为键')
  50. groupId = ObjectIdField(verbose_name=u"分账地址")
  51. logicalCode = StringField(verbose_name=u"设备的逻辑编号")
  52. # 记录着每一笔的明细分成 包括分成方式 分成的金额 以及分成人的身份和ID
  53. partition = ListField(verbose_name=u'收入划分明细')
  54. tags = ListField(verbose_name=u'标签')
  55. desc = StringField(verbose_name=u'描述')
  56. # date = StringField(verbose_name = u'日期文本', default = lambda: datetime.datetime.now().strftime(Const.DATE_FMT))
  57. dateTimeAdded = DateTimeField(verbose_name = u'添加时间', default = datetime.datetime.now)
  58. _shard_key = ('groupId', 'dateTimeAdded')
  59. _origin_meta = {
  60. 'collection': 'dealer_income_proxies',
  61. 'db_alias': 'report'
  62. }
  63. meta = _origin_meta
  64. search_fields = ('title', 'logicalCode')
  65. def __repr__(self): return '<DealerIncomeProxy (%s=%s)>' % (self.source, self.ref_id)
  66. def ref_detail(self, dealerId):
  67. # type: (str)->dict
  68. model = get_income_source_cls(self.source) # type: Union[RechargeRecord, AdRecord, ModelProxy]
  69. partition_dict = {}
  70. ownerId = None
  71. for part in self.partition:
  72. partition_dict[part['id']] = part
  73. if part['role'] == PARTITION_ROLE.OWNER:
  74. ownerId = part['id']
  75. if not ownerId:
  76. raise Exception('no find owner id partition. income proxy id = {}'.format(str(self.id)))
  77. from apps.web.common.proxy import ModelProxy
  78. if issubclass(model, ModelProxy):
  79. record = model.get_one(shard_filter = {'ownerId': ownerId},
  80. id = self.ref_id) # type: Union[RechargeRecord, AdRecord]
  81. rv = record.to_detail()
  82. else:
  83. rv = model.objects(id = self.ref_id).get().to_detail()
  84. # 合伙人可能是后面加入的, 所以在收入MAP里面没有
  85. if str(dealerId) in self.actualAmountMap:
  86. rv['amount'] = self.actualAmountMap[str(dealerId)]
  87. else:
  88. rv['amount'] = RMB(0)
  89. rv.update(
  90. {
  91. 'incomePartitionList': [
  92. {
  93. 'name': Dealer.get_dealer(id_)['nickname'],
  94. 'amount': RMB(amount),
  95. 'role': 'me' if id_ == dealerId else 'partner',
  96. 'owner': partition_dict[id_]['role'] == 'owner', # 加入个字段,标记是否是group owner
  97. 'percent': partition_dict[id_]['share'],
  98. 'id': id_
  99. } for id_, amount in self.actualAmountMap.iteritems()
  100. ] if len(self.actualAmountMap) > 1 else []
  101. }
  102. )
  103. return rv
  104. def to_dict(self, dealerId=None):
  105. # type: (Optional[str])->Dict[str, Union[AnyStr, float]]
  106. rv = {
  107. 'id': str(self.id),
  108. 'title': self.title,
  109. 'totalAmount': RMB(self.totalAmount),
  110. 'amount': RMB(0),
  111. 'createdTime': self.to_datetime_str(self.dateTimeAdded),
  112. 'source': self.source,
  113. 'groupId': str(self.groupId)
  114. }
  115. if dealerId is None:
  116. return rv
  117. else:
  118. if str(dealerId) in self.actualAmountMap:
  119. rv['amount'] = RMB(self.actualAmountMap[str(dealerId)])
  120. return rv
  121. @classmethod
  122. def sum_by_dealer(cls, dealerId, **query):
  123. # type: (ObjectId, dict)->float
  124. query['dealerIds'] = dealerId
  125. return cls.objects(**query).sum('actualAmountMap.%s' % (str(dealerId),))
  126. @staticmethod
  127. def get_agent_partner_amount(ownerId, partitions):
  128. agentAmount, partnerAmount = RMB(0.0), RMB(0.0)
  129. for part in partitions:
  130. if part['role'] == PARTITION_ROLE.AGENT:
  131. agentAmount += part['money']
  132. elif part['id'] != ownerId:
  133. partnerAmount += part['money']
  134. return {'agentAmount': agentAmount, 'partnerAmount': partnerAmount}
  135. @staticmethod
  136. def get_agent_partner_allocated_money(ownerId,partitions,partnerDict):
  137. agentAmount,partnerAmount,ownerAmount = RMB(0.0),RMB(0.0),RMB(0.0)
  138. partnerAmountDict = {}
  139. for part in partitions:
  140. partId = part['id']
  141. partMoney = part['money']
  142. if part['role'] == PARTITION_ROLE.AGENT:
  143. agentAmount += partMoney
  144. elif partId == ownerId:
  145. ownerAmount = partMoney
  146. else:
  147. partnerAmount += part['money']
  148. if partId in partnerDict:
  149. partnerAmountDict[partId] = {'nickname':partnerDict[partId]['name'],'username':partnerDict[partId]['tel'],'money':partMoney}
  150. else:
  151. try:
  152. partner = Dealer.objects.get(id = partId)
  153. partnerAmountDict[partId] = {'username':partner.username,'nickname':partner.nickname,'money':partMoney}
  154. except Exception,e:
  155. continue
  156. return {'agentAmount':agentAmount,'partnerAmount':partnerAmount,'ownerAmount':ownerAmount,'partnerDict':partnerAmountDict}
  157. def update_for_refund(self, refund_fee):
  158. # type:(RMB)->list
  159. '''
  160. 按照分账时相同的方向进行退费
  161. :param refund_fee:
  162. :return:
  163. '''
  164. self.totalAmount = (self.totalAmount - refund_fee)
  165. income_partion = self.partition
  166. owner_partion = []
  167. agent_partion = []
  168. parter_partion = []
  169. platform_partion = []
  170. for item in income_partion:
  171. if item['role'] == PARTITION_ROLE.OWNER:
  172. owner_partion.append(item)
  173. elif item['role'] == PARTITION_ROLE.AGENT:
  174. agent_partion.append(item)
  175. elif item['role'] == PARTITION_ROLE.PARTNER:
  176. parter_partion.append(item)
  177. elif item['role'] == PARTITION_ROLE.PLATFORM:
  178. platform_partion.append(item)
  179. left_refund_fee = refund_fee
  180. refund_partion = []
  181. # 扣除的顺序优先是 平台 代理商 合伙人 经销商
  182. partions = [platform_partion, agent_partion, parter_partion, owner_partion]
  183. #
  184. for partion in partions:
  185. for item in partion:
  186. my_refund = min(refund_fee * Percent(item['share']).as_ratio, RMB(item['money']),
  187. left_refund_fee) # type: RMB
  188. if my_refund > RMB(0):
  189. refund_partion.append({
  190. 'role': item['role'],
  191. 'id': item['id'],
  192. 'amount': my_refund
  193. })
  194. real_money = (RMB(item['money']) - my_refund).mongo_amount
  195. if item['id'] in self.actualAmountMap:
  196. self.actualAmountMap[item['id']] = real_money
  197. item['money'] = real_money
  198. left_refund_fee = left_refund_fee - my_refund
  199. if left_refund_fee < RMB(0):
  200. left_refund_fee = RMB(0)
  201. self.save()
  202. return refund_partion
  203. @property
  204. def statistic_type(self):
  205. return "income"
  206. def get_statistic_update_info(self, amount=None):
  207. hour = self.dateTimeAdded.hour
  208. money = RMB(amount or self.totalAmount).mongo_amount
  209. updateOrInsertData = {
  210. "add_to_set__origin__{}".format(self.statistic_type): self.id,
  211. "inc__daily__{}__{}".format(self.statistic_type, self.source): money,
  212. "inc__hourly__{}__{}__{}".format(hour, self.statistic_type, self.source): money,
  213. "inc__daily__totalIncome": money,
  214. "inc__daily__totalIncomeCount": 1
  215. }
  216. return updateOrInsertData
  217. @property
  218. def partition_map(self):
  219. partitions = self.partition
  220. rv = {}
  221. for role in [PARTITION_ROLE.OWNER, PARTITION_ROLE.AGENT, PARTITION_ROLE.PARTNER]:
  222. rv[role] = []
  223. for partition in partitions:
  224. role = partition['role']
  225. rv[role].append(partition)
  226. return rv
  227. class LedgerInfo(dict):
  228. _LedgerTime = "time"
  229. _Partition = "partition"
  230. _Desc = "desc"
  231. @property
  232. def ledgerTime(self):
  233. return self.get(self._LedgerTime)
  234. @ledgerTime.setter
  235. def ledgerTime(self, value):
  236. self.update({self._LedgerTime: value})
  237. @property
  238. def partition(self):
  239. return self.get(self._Partition)
  240. @partition.setter
  241. def partition(self, value):
  242. self.update({self._Partition: value})
  243. @property
  244. def desc(self):
  245. return self.get(self._Desc)
  246. @desc.setter
  247. def desc(self, value):
  248. self.update({self._Desc: value})
  249. class DealerGroupStats(Searchable):
  250. """
  251. 经销商的每日的地址收益的统计值
  252. """
  253. date = StringField(verbose_name="日期 单位天", required=True)
  254. dealerId = StringField(verbose_name="经销商ID", required=True)
  255. groupId = StringField(verbose_name="地址ID", required=True)
  256. orderCount = IntField(verbose_name=u"累计订单数量")
  257. elecCount = FloatField(verbose_name=u"累计耗电量")
  258. amount = MonetaryField(verbose_name=u"累计收益")
  259. bestowAmount = MonetaryField(verbose_name=u"累计消费赠送金额")
  260. duration = IntField(verbose_name=u"累计充电时长")
  261. withdrawSourceKey = StringField(verbose_name=u"提现网关")
  262. ledgerInfo = DictField()
  263. dateTimeAdded = DateTimeField(verbose_name=u"添加时间", default=datetime.datetime.now)
  264. """
  265. 对于订单的记录 最好是维护一个队列 在队列里面进行处理
  266. """
  267. def __str__(self):
  268. return "{}-{}".format(self.__class__, str(self.id))
  269. @classmethod
  270. def update_group_stats(cls, group, order, date=None): # type:(GroupDict, ConsumeRecord, datetime.datetime) -> Optional[DealerGroupStats, None]
  271. """
  272. 更新当日的数据 如果更新成功 即返回记录供order建立引用关系
  273. 如果建立失败
  274. """
  275. dealer = order.owner
  276. app = LedgerConsumeApp.get_app(dealer)
  277. gateway = app.new_gateway("")
  278. date = (date or datetime.datetime.today()).strftime("%Y-%m-%d")
  279. result = cls.objects.filter(
  280. date=date,
  281. groupId=group.groupId,
  282. dealerId=group.ownerId
  283. ).update_one(
  284. upsert=True,
  285. inc__orderCount=1,
  286. inc__elecCount=order.service.elec,
  287. inc__amount=order.actualAmount,
  288. inc__bestowAmount=order.bestowAmount,
  289. inc__duration=order.service.duration,
  290. withdrawSourceKey=gateway.withdraw_source_key()
  291. )
  292. if result:
  293. return cls.objects.get(date=date, groupId=group.groupId)
  294. else:
  295. return None
  296. @property
  297. def ledger(self): # type:() -> LedgerInfo
  298. return LedgerInfo(self.ledgerInfo)
  299. @property
  300. def is_ledgered(self):
  301. return bool(self.ledger.ledgerTime)
  302. @property
  303. def ledger_enable(self):
  304. """
  305. 需要等每日完成之后才可以结算
  306. """
  307. return datetime.date.today() > datetime.datetime.strptime(self.date, "%Y-%m-%d").date()
  308. @property
  309. def group(self): # type:()-> GroupDict
  310. return Group.get_group(self.groupId)
  311. @property
  312. def dealer(self):
  313. return Dealer.objects.filter(id=self.dealerId).first()
  314. def set_ledgered(self, time):
  315. """
  316. 设置
  317. """
  318. if not self.ledger:
  319. return
  320. ledgerInfo = self.ledger
  321. ledgerInfo.ledgerTime = time
  322. self.ledgerInfo = ledgerInfo
  323. return self.save()
  324. def set_partition(self, partition):
  325. ledgerInfo = self.ledger
  326. ledgerInfo.partition = partition
  327. self.ledgerInfo = ledgerInfo
  328. return self.save()
  329. def set_description(self, desc):
  330. ledgerInfo = self.ledger
  331. ledgerInfo.desc = desc
  332. self.ledgerInfo = ledgerInfo
  333. return self.save()
  334. def record_income_proxy(source, record, partitionMap=None, dateTime=None):
  335. # type: (str, RechargeRecord, dict, datetime.datetime)->Optional[DealerIncomeProxy]
  336. """
  337. 记录收益代理,会记录分成的情况,需要验证比例是合法的, 包括代理商分成经销商的情况
  338. e.g [{'role': 'agent', 'percent': 80, 'id': ''}, {'role': 'owner', 'percent': 20, 'id': ''}]
  339. 代理需要记录参与收益的经销商ID列表
  340. :param source:
  341. :param record:
  342. :param partitionMap: 以前是分账列表 目前项目不需要这个参数 用户充值的金额全额进入经销商的资金账户
  343. :param dateTime:
  344. :return:
  345. """
  346. try:
  347. if not dateTime:
  348. if not record.finishedTime:
  349. dateTime = datetime.datetime.now()
  350. else:
  351. dateTime = record.finishedTime
  352. logger.debug('record_income_proxy source=%s record=%r partitionMap=%s dateTime=%s' % (source, record, partitionMap, dateTime))
  353. validIncomeSources = DEALER_INCOME_SOURCE.choices()
  354. if source not in validIncomeSources:
  355. raise ParameterError('invalid source, only %s are supported, %s given' % (validIncomeSources, source))
  356. # 将map的结构转换为list的结构
  357. partition = [{
  358. "role": "owner",
  359. "money": record.mongo_amount,
  360. "id": record.ownerId,
  361. "share": Ratio(100).mongo_amount
  362. }]
  363. def get_dealer_money_map():
  364. return {
  365. record.ownerId: record.mongo_amount
  366. }
  367. # : 存储代理
  368. proxy = DealerIncomeProxy(
  369. ref_id=record.id,
  370. dealerIds=[ObjectId(record.ownerId)],
  371. partition=partition,
  372. groupId=ObjectId(record.groupId),
  373. logicalCode=record.logicalCode,
  374. title=record.subject,
  375. source=source,
  376. totalAmount=record.mongo_amount,
  377. actualAmountMap=get_dealer_money_map(),
  378. dateTimeAdded=dateTime)
  379. proxy.save()
  380. return proxy
  381. except Exception as e:
  382. logger.exception('cannot record dealers income, error=%s, record=%s' % (e, record))
  383. raise e