ledger.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. import datetime
  4. import logging
  5. from collections import namedtuple, defaultdict
  6. from typing import Optional, TYPE_CHECKING
  7. from apilib.monetary import RMB, Percent
  8. from apilib.utils import flatten
  9. from apps.web.agent.models import Agent
  10. from apps.web.constant import PARTITION_ROLE, RechargeRecordVia
  11. from apps.web.core import PayAppType
  12. from apps.web.core.payment import PaymentGateway
  13. from apps.web.dealer.define import DealerConst, DEALER_INCOME_TYPE
  14. from apps.web.dealer.models import Dealer
  15. from apps.web.dealer.proxy import record_income_proxy
  16. from apps.web.device.models import GroupDict
  17. from apps.web.report.utils import CentralDataProcessor
  18. if TYPE_CHECKING:
  19. from apps.web.user.models import RechargeRecord
  20. from apps.web.dealer.proxy import DealerGroupStats
  21. from apps.web.dealer.models import DealerDict
  22. logger = logging.getLogger(__name__)
  23. class Ledger(object):
  24. """
  25. 对用户扫码使用或者充值金额进行分账
  26. """
  27. def __init__(self, source, record, notify=True, ledgerTime=None):
  28. # type: (str, RechargeRecord, bool, Optional[datetime])->None
  29. self.journal_logger = logging.getLogger('ledger')
  30. self.source = source # 指的是用户的充值目的
  31. self.record = record
  32. self.money = record.money
  33. self.group = record.group
  34. self.dealer = self.record.owner # type: Dealer
  35. self.agent = Agent.objects(id=self.dealer.agentId).get() # type: Agent
  36. self.notify = notify
  37. if ledgerTime:
  38. self.ledgerTime = ledgerTime
  39. elif self.record.finishedTime:
  40. self.ledgerTime = self.record.finishedTime
  41. else:
  42. self.ledgerTime = datetime.datetime.now()
  43. def __repr__(self):
  44. return '<%s source=%s record=%r>' % (self.__class__.__name__, self.source, self.record)
  45. def execute(self, journal=False, stats=False, check=False):
  46. # type: (bool, bool, bool)->None
  47. # 订单的分账永远只有经销商一个人
  48. partition_map = self.record.partition_map
  49. if journal:
  50. self.journal_logger.info(
  51. 'ledger report: record = {}; source = {}; group = {}; '
  52. 'money = {}; partition = {}; isLedgered = {}; time = {}'.format(
  53. str(self.record.id), self.source, self.record.group.groupId,
  54. self.money, partition_map, self.record.is_ledgered, self.ledgerTime
  55. )
  56. )
  57. if not self.record.is_success:
  58. logger.debug('{} is not success.'.format(repr(self.record)))
  59. return
  60. if self.record.is_ledgered:
  61. logger.debug('{} has been leger.'.format(repr(self.record)))
  62. return
  63. if self.record.via == RechargeRecordVia.RefundCash:
  64. if self.record.money > RMB(0):
  65. logger.debug('ledger money is more zero in refundCash. record = {}'.format(repr(self.record)))
  66. return
  67. else:
  68. if self.record.money < RMB(0):
  69. logger.debug('ledger money is less zero. record = {}'.format(repr(self.record)))
  70. return
  71. if not self.record.ledger_enable:
  72. logger.debug('status of {} is not ledger enable.'.format(repr(self.record)))
  73. return
  74. proxy = record_income_proxy(self.source, self.record, dateTime=self.ledgerTime)
  75. if stats:
  76. if not self.record.logicalCode:
  77. allowed = {'dealer': True, 'group': True, 'device': False}
  78. CentralDataProcessor(record=proxy, check=check, allowed=allowed).process()
  79. else:
  80. CentralDataProcessor(record=proxy, check=check).process()
  81. self.record_balance()
  82. self.record.set_ledgered()
  83. def __need_ledger(self, entity, share_money):
  84. if share_money == RMB(0):
  85. logger.debug('share money for {} is zero.'.format(str(entity)))
  86. return False
  87. if self.record.via == RechargeRecordVia.RefundCash:
  88. if share_money > RMB(0):
  89. logger.debug('refund share money for {} is bigger than zero({}).'.format(str(entity), str(share_money)))
  90. return False
  91. else:
  92. if share_money < RMB(0):
  93. logger.debug('refund share money for {} is less than zero({}).'.format(str(entity), str(share_money)))
  94. return False
  95. return True
  96. def record_balance(self):
  97. self.__record_dealer_balance()
  98. def __record_dealer_balance(self):
  99. from taskmanager.mediator import task_caller
  100. source_key = self.record.withdrawSourceKey
  101. source = DealerConst.MAP_USER_SOURCE_TO_DEALER_SOURCE[self.source]
  102. infoDict = self.record.to_json_dict()
  103. owner = self.record.owner
  104. share_money = self.record.money
  105. logger.info('[recordDealerIncome] dealer(id={}), amount={}, source={}'.format(owner.id, share_money, source))
  106. if not self.__need_ledger('owner<id={}>'.format(str(owner.id)), share_money):
  107. return
  108. logger.info('record owner(id=%s) balance, amount=%s' % (str(owner.id), share_money))
  109. owner.record_income(source, source_key, share_money)
  110. infoDict.update({'ownerId': owner.id})
  111. if self.notify and owner.newUserPaymentOrderPushSwitch:
  112. task_caller('report_new_payment_to_dealer_via_wechat', record = infoDict)
  113. ShareItem = namedtuple("ShareItem", ["role", "id", "share", "money"])
  114. class IncomeStrategy(object):
  115. """
  116. 用户每支付一次 都需要进行一笔分账 分账的基本顺序如下
  117. 首先 是 运营的平台部分收取 收取方式分为 固定或者百分比 对象是用户支付的总金额
  118. 然后 是 代理商的部分收取 收取方式为 固定或者百分比 对象是用户支付的总金额
  119. 这部分收取完之后 才会是经销商实际应该收的总金额 然后是经销商和合伙人进行分
  120. 先按百分比分合伙人的 最后的就是经销商实际应得的
  121. """
  122. def __init__(self, dealer, group, record, payment_gateway=None):
  123. assert record.via != RechargeRecordVia.Mix, u'混合订单不需要计算分账MAP'
  124. self._dealer = dealer # type: Dealer
  125. self._group = group # type: GroupDict
  126. self._record = record # type: RechargeRecord
  127. self._payment_gateway = payment_gateway # type: PaymentGateway
  128. def calc_account_split_map(self): # type:() -> dict
  129. raise NotImplementedError(u"尚未实现")
  130. @property
  131. def agent_profit_share(self):
  132. """
  133. 代理商的分成比例分为两种 由订单本身的属性决定
  134. 一种是 普通的经营分成比例
  135. 一种是 经销商商户收款模式下的经营分成比例
  136. """
  137. if 'isBt' in self._record.attachParas and self._record.attachParas['isBt']:
  138. return self._dealer.agentProfitShare
  139. if not self._payment_gateway:
  140. self._payment_gateway = PaymentGateway.clone_from_order(self._record) # type: PaymentGateway
  141. return self._dealer.agentProfitShare
  142. @property
  143. def manager_profit_share(self):
  144. """
  145. 厂商的分成比例 目前仅允许资金池的分账
  146. 如果厂商配置了分账 直接抛出异常即可
  147. """
  148. if not self._payment_gateway:
  149. self._payment_gateway = PaymentGateway.clone_from_order(self._record) # type: PaymentGateway
  150. return self._dealer.my_agent.managerProfitShare
  151. @staticmethod
  152. def check_partition_map(partitionMap):
  153. partitions = list(flatten(partitionMap.values()))
  154. sumShares = Percent(0)
  155. for _item in partitions:
  156. # 验算范围
  157. if Percent(_item['share']) < Percent(0):
  158. raise ValueError('partition share must gte 0!')
  159. sumShares += Percent(_item["share"])
  160. # 验算累加和
  161. if sumShares != Percent(100):
  162. raise ValueError('sum of shares has to be 100')
  163. return partitionMap
  164. class AgentLedgerFirst(IncomeStrategy):
  165. """
  166. 层级结构的分成比例
  167. 依次是 厂商 --- 代理商 --- 经销商(合伙人)
  168. 每层分账的比例 为上一层分成比例的剩下值
  169. 举例
  170. 厂商设置自己 收益10%
  171. 代理商设置自己收益10%
  172. 则厂商的收益为 10%
  173. 代理商收益为 (1- 10%) * 10% = 9%
  174. 经销商的收益为 1- 10% - 9% = 81%
  175. """
  176. def calc_account_split_map(self): # type:() -> dict
  177. # 保持和之前的一致 优先提取下参数
  178. dealerId = str(self._dealer.id)
  179. agentId = self._dealer.agentId
  180. group = self._group
  181. # 厂商的分成比例构建 实际使用primaryAgent代替
  182. primaryAgentId = str(self._dealer.my_agent.manager.primeAgentId)
  183. managerProfitShare = self.manager_profit_share
  184. managerShare = {'role': PARTITION_ROLE.AGENT, 'id': primaryAgentId, 'share': managerProfitShare.mongo_amount}
  185. # 代理商的分成比例构建
  186. discount_multiplier = Percent(100) - managerProfitShare
  187. agentProfitShare = self.agent_profit_share * discount_multiplier
  188. agentShare = {'role': PARTITION_ROLE.AGENT, 'id': agentId, 'share': agentProfitShare.mongo_amount}
  189. # 合伙人分成比例的构建
  190. discount_multiplier = discount_multiplier - agentProfitShare
  191. partners = group['partnerDict'].values()
  192. partnerPartition = [
  193. {
  194. 'role': PARTITION_ROLE.PARTNER,
  195. 'id': partner['id'],
  196. 'share': (discount_multiplier * Percent(partner['percent'])).mongo_amount
  197. }
  198. for partner in partners
  199. ]
  200. partnerShares = sum((Percent(_['share']) for _ in partnerPartition), Percent(0))
  201. dealerShare = discount_multiplier - partnerShares
  202. ownerShare = {
  203. 'role': PARTITION_ROLE.OWNER,
  204. 'id': dealerId,
  205. 'share': dealerShare.mongo_amount
  206. }
  207. partitionMap = {
  208. PARTITION_ROLE.AGENT: [managerShare, agentShare],
  209. PARTITION_ROLE.PARTNER: partnerPartition,
  210. PARTITION_ROLE.OWNER: [ownerShare]
  211. }
  212. return self.check_partition_map(partitionMap)
  213. class PartnerLedgerFirst(IncomeStrategy):
  214. """
  215. 平级结构的分成
  216. 所有角色共分比例 100%
  217. 举例
  218. 厂商设置自己 收益10%
  219. 代理商设置自己收益10%
  220. 则厂商的收益为 10%
  221. 代理商收益为 10
  222. 经销商的收益为 1- 10% - 10% = 80%
  223. """
  224. def calc_account_split_map(self): # type:() -> dict
  225. dealerId = str(self._dealer.id)
  226. group = self._group
  227. # 计算合伙人所有产生的收益
  228. partners = group['partnerDict'].values()
  229. partnerPartition = [
  230. {
  231. 'role': PARTITION_ROLE.PARTNER,
  232. 'id': partner['id'],
  233. 'share': Percent(partner['percent']).mongo_amount
  234. }
  235. for partner in partners
  236. ]
  237. partnerProfitShare = sum((Percent(_['share']) for _ in partnerPartition), Percent(0))
  238. # 计算代理商产生的收益比例
  239. agentId = self._dealer.agentId
  240. agentProfitShare = self.agent_profit_share
  241. agentShare = {'role': PARTITION_ROLE.AGENT, 'id': agentId, 'share': agentProfitShare.mongo_amount}
  242. # 计算厂商所产生的收益
  243. primaryAgentId = str(self._dealer.my_agent.manager.primeAgentId)
  244. managerProfitShare = self.manager_profit_share
  245. managerShare = {'role': PARTITION_ROLE.AGENT, 'id': primaryAgentId, 'share': managerProfitShare.mongo_amount}
  246. # 最后经销商就是剩下的比例
  247. dealerProfitShare = Percent(100) - managerProfitShare - agentProfitShare - partnerProfitShare
  248. ownerShare = {
  249. 'role': PARTITION_ROLE.OWNER,
  250. 'id': str(dealerId),
  251. 'share': dealerProfitShare.mongo_amount
  252. }
  253. partitionMap = {
  254. PARTITION_ROLE.AGENT: [managerShare, agentShare],
  255. PARTITION_ROLE.PARTNER: partnerPartition,
  256. PARTITION_ROLE.OWNER: [ownerShare]
  257. }
  258. return self.check_partition_map(partitionMap)
  259. class LedgerConsumeOrder(object):
  260. """
  261. 对每日的消费订单统计进行分润
  262. """
  263. _source = "ledger_consume"
  264. def __init__(self, statsRecord, ledgerTime=None): # type: (DealerGroupStats, str) -> None
  265. self._record = statsRecord
  266. self._time = ledgerTime
  267. self._owner = self._record.dealer # type: Dealer
  268. self._source_key = self._record.withdrawSourceKey
  269. def execute(self):
  270. logger.info("[LedgerConsumeOrder] stats = {}".format(self._record))
  271. if self._record.is_ledgered:
  272. logger.warning("[LedgerConsumeOrder] stats <{}> has been ledgered!".format(self._record))
  273. return
  274. # if not self._record.ledger_enable:
  275. # logger.warning("[LedgerConsumeOrder] stats <{}> not allow ledger!".format(self._record))
  276. # return
  277. # TODO 每日统计等等统计暂时不处理
  278. # 创建分账以及收益信息
  279. partition = self._get_partition_map()
  280. self._record.set_partition(partition)
  281. if self._owner.sub_balance(DEALER_INCOME_TYPE.DEVICE_INCOME) < self._record.amount:
  282. self._record.set_description(u"资金账户余额不足,分账失败")
  283. return
  284. # 记录消费收益之前 首先从经销商的资金池里面扣除 然后再对消费进行分钱 收益前先冻结 经销商的资金池 完成之后再清除
  285. self._owner.freeze_ledger_balance(self._record.amount, self._source_key, str(self._record.id))
  286. self._record_balance(partition)
  287. self._owner.clear_ledger_balance(str(self._record.id))
  288. # 设置分账标志
  289. t = self._time or datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  290. self._record.set_ledgered(t)
  291. def get_partition_map(self):
  292. return self._get_partition_map()
  293. def _get_elec_payer(self, partners): # type:(list) -> dict
  294. payer = {}
  295. for _partner in partners:
  296. if _partner["payElecFee"]:
  297. if payer:
  298. raise ValueError("payer count gte 1! record = {}".format(self._record))
  299. payer = _partner
  300. return payer
  301. def _get_partition_map(self): # type:() -> list
  302. group = self._record.group # type: GroupDict
  303. partners = group.partners
  304. elecFee = group.elecFee * self._record.elecCount # type: RMB
  305. if elecFee < RMB(0):
  306. raise ValueError("elec lt 0, record = {}".format(self._record))
  307. if self._record.amount < RMB(0):
  308. raise ValueError("totalAmount lt 0, record = {}".format(self._record))
  309. # 找出电费承担方 如果电费承担不存在 则集体承担
  310. # 不对totalAmount做非0校验 这个数值确实可能小于0 比如全部使用的是赠送金额的钱 但是存在电费
  311. elecPayer = self._get_elec_payer(partners)
  312. totalAmount = self._record.amount - elecFee if elecPayer else self._record.amount
  313. totalMoney, totalP, partition = RMB(0), Percent(0), list()
  314. for _partner in partners:
  315. _m = totalAmount * Percent(_partner["percent"]).as_ratio
  316. _s = Percent(_partner["percent"])
  317. _p = {
  318. "role": PARTITION_ROLE.PARTNER,
  319. "id": _partner["id"],
  320. "share": _s.mongo_amount,
  321. "money": _m.mongo_amount
  322. }
  323. if elecPayer and _p["id"] == elecPayer["id"]:
  324. _p.update({
  325. "elecFee": elecFee.mongo_amount
  326. })
  327. totalMoney += _m
  328. totalP += _s
  329. partition.append(_p)
  330. if abs(totalMoney) > abs(totalAmount):
  331. raise ValueError("total share money <{}> gt total amount <{}>, record = {}".format(totalMoney, totalAmount, self._record))
  332. if totalP > Percent(100):
  333. raise ValueError(u"total share percent gt 100, record = {}".format(self._record))
  334. partition.append({
  335. "role": PARTITION_ROLE.OWNER,
  336. "id": str(self._record.dealerId),
  337. "share": (Percent(100) - totalP).mongo_amount,
  338. "money": (totalAmount - totalMoney).mongo_amount
  339. })
  340. # TODO 最后再校验一次是否有必要
  341. logger.info('[_get_partition_map] record = {}, get partition = {}'.format(self._record, partition))
  342. return partition
  343. def _record_balance(self, partition):
  344. """
  345. 根据收益的划分 对经销商 以及合伙人 收益进行增加
  346. """
  347. for _owner in partition:
  348. _share_money = RMB(_owner['money']) + RMB(_owner.get("elecFee", 0))
  349. _dealer = Dealer.objects.get(id=_owner["id"])
  350. self._record_dealer_balance(_dealer, _share_money)
  351. def _record_dealer_balance(self, dealer, money):
  352. logger.info("[_record_dealer_balance] dealer = {}, money ={}, record = {}".format(
  353. dealer, money, self._record
  354. ))
  355. if money == RMB(0):
  356. return
  357. dealer.record_income(
  358. source=self._source,
  359. source_key=self._source_key,
  360. money=money
  361. )