123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- # -*- coding: utf-8 -*-
- # !/usr/bin/env python
- import datetime
- import logging
- from collections import namedtuple, defaultdict
- from typing import Optional, TYPE_CHECKING
- from apilib.monetary import RMB, Percent
- from apilib.utils import flatten
- from apps.web.agent.models import Agent
- from apps.web.constant import PARTITION_ROLE, RechargeRecordVia
- from apps.web.core import PayAppType
- from apps.web.core.payment import PaymentGateway
- from apps.web.dealer.define import DealerConst, DEALER_INCOME_TYPE
- from apps.web.dealer.models import Dealer
- from apps.web.dealer.proxy import record_income_proxy
- from apps.web.device.models import GroupDict
- from apps.web.report.utils import CentralDataProcessor
- if TYPE_CHECKING:
- from apps.web.user.models import RechargeRecord
- from apps.web.dealer.proxy import DealerGroupStats
- from apps.web.dealer.models import DealerDict
- logger = logging.getLogger(__name__)
- class Ledger(object):
- """
- 对用户扫码使用或者充值金额进行分账
- """
- def __init__(self, source, record, notify=True, ledgerTime=None):
- # type: (str, RechargeRecord, bool, Optional[datetime])->None
- self.journal_logger = logging.getLogger('ledger')
- self.source = source # 指的是用户的充值目的
- self.record = record
- self.money = record.money
- self.group = record.group
- self.dealer = self.record.owner # type: Dealer
- self.agent = Agent.objects(id=self.dealer.agentId).get() # type: Agent
- self.notify = notify
- if ledgerTime:
- self.ledgerTime = ledgerTime
- elif self.record.finishedTime:
- self.ledgerTime = self.record.finishedTime
- else:
- self.ledgerTime = datetime.datetime.now()
- def __repr__(self):
- return '<%s source=%s record=%r>' % (self.__class__.__name__, self.source, self.record)
- def execute(self, journal=False, stats=False, check=False):
- # type: (bool, bool, bool)->None
- # 订单的分账永远只有经销商一个人
- partition_map = self.record.partition_map
- if journal:
- self.journal_logger.info(
- 'ledger report: record = {}; source = {}; group = {}; '
- 'money = {}; partition = {}; isLedgered = {}; time = {}'.format(
- str(self.record.id), self.source, self.record.group.groupId,
- self.money, partition_map, self.record.is_ledgered, self.ledgerTime
- )
- )
- if not self.record.is_success:
- logger.debug('{} is not success.'.format(repr(self.record)))
- return
- if self.record.is_ledgered:
- logger.debug('{} has been leger.'.format(repr(self.record)))
- return
- if self.record.via == RechargeRecordVia.RefundCash:
- if self.record.money > RMB(0):
- logger.debug('ledger money is more zero in refundCash. record = {}'.format(repr(self.record)))
- return
- else:
- if self.record.money < RMB(0):
- logger.debug('ledger money is less zero. record = {}'.format(repr(self.record)))
- return
- if not self.record.ledger_enable:
- logger.debug('status of {} is not ledger enable.'.format(repr(self.record)))
- return
- proxy = record_income_proxy(self.source, self.record, dateTime=self.ledgerTime)
- if stats:
- if not self.record.logicalCode:
- allowed = {'dealer': True, 'group': True, 'device': False}
- CentralDataProcessor(record=proxy, check=check, allowed=allowed).process()
- else:
- CentralDataProcessor(record=proxy, check=check).process()
- self.record_balance()
- self.record.set_ledgered()
- def __need_ledger(self, entity, share_money):
- if share_money == RMB(0):
- logger.debug('share money for {} is zero.'.format(str(entity)))
- return False
- if self.record.via == RechargeRecordVia.RefundCash:
- if share_money > RMB(0):
- logger.debug('refund share money for {} is bigger than zero({}).'.format(str(entity), str(share_money)))
- return False
- else:
- if share_money < RMB(0):
- logger.debug('refund share money for {} is less than zero({}).'.format(str(entity), str(share_money)))
- return False
- return True
- def record_balance(self):
- self.__record_dealer_balance()
- def __record_dealer_balance(self):
- from taskmanager.mediator import task_caller
- source_key = self.record.withdrawSourceKey
- source = DealerConst.MAP_USER_SOURCE_TO_DEALER_SOURCE[self.source]
- infoDict = self.record.to_json_dict()
- owner = self.record.owner
- share_money = self.record.money
- logger.info('[recordDealerIncome] dealer(id={}), amount={}, source={}'.format(owner.id, share_money, source))
- if not self.__need_ledger('owner<id={}>'.format(str(owner.id)), share_money):
- return
- logger.info('record owner(id=%s) balance, amount=%s' % (str(owner.id), share_money))
- owner.record_income(source, source_key, share_money)
- infoDict.update({'ownerId': owner.id})
- if self.notify and owner.newUserPaymentOrderPushSwitch:
- task_caller('report_new_payment_to_dealer_via_wechat', record = infoDict)
- ShareItem = namedtuple("ShareItem", ["role", "id", "share", "money"])
- class IncomeStrategy(object):
- """
- 用户每支付一次 都需要进行一笔分账 分账的基本顺序如下
- 首先 是 运营的平台部分收取 收取方式分为 固定或者百分比 对象是用户支付的总金额
- 然后 是 代理商的部分收取 收取方式为 固定或者百分比 对象是用户支付的总金额
- 这部分收取完之后 才会是经销商实际应该收的总金额 然后是经销商和合伙人进行分
- 先按百分比分合伙人的 最后的就是经销商实际应得的
- """
- def __init__(self, dealer, group, record, payment_gateway=None):
- assert record.via != RechargeRecordVia.Mix, u'混合订单不需要计算分账MAP'
- self._dealer = dealer # type: Dealer
- self._group = group # type: GroupDict
- self._record = record # type: RechargeRecord
- self._payment_gateway = payment_gateway # type: PaymentGateway
- def calc_account_split_map(self): # type:() -> dict
- raise NotImplementedError(u"尚未实现")
- @property
- def agent_profit_share(self):
- """
- 代理商的分成比例分为两种 由订单本身的属性决定
- 一种是 普通的经营分成比例
- 一种是 经销商商户收款模式下的经营分成比例
- """
- if 'isBt' in self._record.attachParas and self._record.attachParas['isBt']:
- return self._dealer.agentProfitShare
- if not self._payment_gateway:
- self._payment_gateway = PaymentGateway.clone_from_order(self._record) # type: PaymentGateway
- return self._dealer.agentProfitShare
- @property
- def manager_profit_share(self):
- """
- 厂商的分成比例 目前仅允许资金池的分账
- 如果厂商配置了分账 直接抛出异常即可
- """
- if not self._payment_gateway:
- self._payment_gateway = PaymentGateway.clone_from_order(self._record) # type: PaymentGateway
- return self._dealer.my_agent.managerProfitShare
- @staticmethod
- def check_partition_map(partitionMap):
- partitions = list(flatten(partitionMap.values()))
- sumShares = Percent(0)
- for _item in partitions:
- # 验算范围
- if Percent(_item['share']) < Percent(0):
- raise ValueError('partition share must gte 0!')
- sumShares += Percent(_item["share"])
- # 验算累加和
- if sumShares != Percent(100):
- raise ValueError('sum of shares has to be 100')
- return partitionMap
- class AgentLedgerFirst(IncomeStrategy):
- """
- 层级结构的分成比例
- 依次是 厂商 --- 代理商 --- 经销商(合伙人)
- 每层分账的比例 为上一层分成比例的剩下值
- 举例
- 厂商设置自己 收益10%
- 代理商设置自己收益10%
- 则厂商的收益为 10%
- 代理商收益为 (1- 10%) * 10% = 9%
- 经销商的收益为 1- 10% - 9% = 81%
- """
- def calc_account_split_map(self): # type:() -> dict
- # 保持和之前的一致 优先提取下参数
- dealerId = str(self._dealer.id)
- agentId = self._dealer.agentId
- group = self._group
- # 厂商的分成比例构建 实际使用primaryAgent代替
- primaryAgentId = str(self._dealer.my_agent.manager.primeAgentId)
- managerProfitShare = self.manager_profit_share
- managerShare = {'role': PARTITION_ROLE.AGENT, 'id': primaryAgentId, 'share': managerProfitShare.mongo_amount}
- # 代理商的分成比例构建
- discount_multiplier = Percent(100) - managerProfitShare
- agentProfitShare = self.agent_profit_share * discount_multiplier
- agentShare = {'role': PARTITION_ROLE.AGENT, 'id': agentId, 'share': agentProfitShare.mongo_amount}
- # 合伙人分成比例的构建
- discount_multiplier = discount_multiplier - agentProfitShare
- partners = group['partnerDict'].values()
- partnerPartition = [
- {
- 'role': PARTITION_ROLE.PARTNER,
- 'id': partner['id'],
- 'share': (discount_multiplier * Percent(partner['percent'])).mongo_amount
- }
- for partner in partners
- ]
- partnerShares = sum((Percent(_['share']) for _ in partnerPartition), Percent(0))
- dealerShare = discount_multiplier - partnerShares
- ownerShare = {
- 'role': PARTITION_ROLE.OWNER,
- 'id': dealerId,
- 'share': dealerShare.mongo_amount
- }
- partitionMap = {
- PARTITION_ROLE.AGENT: [managerShare, agentShare],
- PARTITION_ROLE.PARTNER: partnerPartition,
- PARTITION_ROLE.OWNER: [ownerShare]
- }
- return self.check_partition_map(partitionMap)
- class PartnerLedgerFirst(IncomeStrategy):
- """
- 平级结构的分成
- 所有角色共分比例 100%
- 举例
- 厂商设置自己 收益10%
- 代理商设置自己收益10%
- 则厂商的收益为 10%
- 代理商收益为 10
- 经销商的收益为 1- 10% - 10% = 80%
- """
- def calc_account_split_map(self): # type:() -> dict
- dealerId = str(self._dealer.id)
- group = self._group
- # 计算合伙人所有产生的收益
- partners = group['partnerDict'].values()
- partnerPartition = [
- {
- 'role': PARTITION_ROLE.PARTNER,
- 'id': partner['id'],
- 'share': Percent(partner['percent']).mongo_amount
- }
- for partner in partners
- ]
- partnerProfitShare = sum((Percent(_['share']) for _ in partnerPartition), Percent(0))
- # 计算代理商产生的收益比例
- agentId = self._dealer.agentId
- agentProfitShare = self.agent_profit_share
- agentShare = {'role': PARTITION_ROLE.AGENT, 'id': agentId, 'share': agentProfitShare.mongo_amount}
- # 计算厂商所产生的收益
- primaryAgentId = str(self._dealer.my_agent.manager.primeAgentId)
- managerProfitShare = self.manager_profit_share
- managerShare = {'role': PARTITION_ROLE.AGENT, 'id': primaryAgentId, 'share': managerProfitShare.mongo_amount}
- # 最后经销商就是剩下的比例
- dealerProfitShare = Percent(100) - managerProfitShare - agentProfitShare - partnerProfitShare
- ownerShare = {
- 'role': PARTITION_ROLE.OWNER,
- 'id': str(dealerId),
- 'share': dealerProfitShare.mongo_amount
- }
- partitionMap = {
- PARTITION_ROLE.AGENT: [managerShare, agentShare],
- PARTITION_ROLE.PARTNER: partnerPartition,
- PARTITION_ROLE.OWNER: [ownerShare]
- }
- return self.check_partition_map(partitionMap)
- class LedgerConsumeOrder(object):
- """
- 对每日的消费订单统计进行分润
- """
- _source = "ledger_consume"
- def __init__(self, statsRecord, ledgerTime=None): # type: (DealerGroupStats, str) -> None
- self._record = statsRecord
- self._time = ledgerTime
- self._owner = self._record.dealer # type: Dealer
- self._source_key = self._record.withdrawSourceKey
- def execute(self):
- logger.info("[LedgerConsumeOrder] stats = {}".format(self._record))
- if self._record.is_ledgered:
- logger.warning("[LedgerConsumeOrder] stats <{}> has been ledgered!".format(self._record))
- return
- # if not self._record.ledger_enable:
- # logger.warning("[LedgerConsumeOrder] stats <{}> not allow ledger!".format(self._record))
- # return
- # TODO 每日统计等等统计暂时不处理
- # 创建分账以及收益信息
- partition = self._get_partition_map()
- self._record.set_partition(partition)
- if self._owner.sub_balance(DEALER_INCOME_TYPE.DEVICE_INCOME) < self._record.amount:
- self._record.set_description(u"资金账户余额不足,分账失败")
- return
- # 记录消费收益之前 首先从经销商的资金池里面扣除 然后再对消费进行分钱 收益前先冻结 经销商的资金池 完成之后再清除
- self._owner.freeze_ledger_balance(self._record.amount, self._source_key, str(self._record.id))
- self._record_balance(partition)
- self._owner.clear_ledger_balance(str(self._record.id))
- # 设置分账标志
- t = self._time or datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- self._record.set_ledgered(t)
- def get_partition_map(self):
- return self._get_partition_map()
- def _get_elec_payer(self, partners): # type:(list) -> dict
- payer = {}
- for _partner in partners:
- if _partner["payElecFee"]:
- if payer:
- raise ValueError("payer count gte 1! record = {}".format(self._record))
- payer = _partner
- return payer
- def _get_partition_map(self): # type:() -> list
- group = self._record.group # type: GroupDict
- partners = group.partners
- elecFee = group.elecFee * self._record.elecCount # type: RMB
- if elecFee < RMB(0):
- raise ValueError("elec lt 0, record = {}".format(self._record))
- if self._record.amount < RMB(0):
- raise ValueError("totalAmount lt 0, record = {}".format(self._record))
- # 找出电费承担方 如果电费承担不存在 则集体承担
- # 不对totalAmount做非0校验 这个数值确实可能小于0 比如全部使用的是赠送金额的钱 但是存在电费
- elecPayer = self._get_elec_payer(partners)
- totalAmount = self._record.amount - elecFee if elecPayer else self._record.amount
- totalMoney, totalP, partition = RMB(0), Percent(0), list()
- for _partner in partners:
- _m = totalAmount * Percent(_partner["percent"]).as_ratio
- _s = Percent(_partner["percent"])
- _p = {
- "role": PARTITION_ROLE.PARTNER,
- "id": _partner["id"],
- "share": _s.mongo_amount,
- "money": _m.mongo_amount
- }
- if elecPayer and _p["id"] == elecPayer["id"]:
- _p.update({
- "elecFee": elecFee.mongo_amount
- })
- totalMoney += _m
- totalP += _s
- partition.append(_p)
- if abs(totalMoney) > abs(totalAmount):
- raise ValueError("total share money <{}> gt total amount <{}>, record = {}".format(totalMoney, totalAmount, self._record))
- if totalP > Percent(100):
- raise ValueError(u"total share percent gt 100, record = {}".format(self._record))
- partition.append({
- "role": PARTITION_ROLE.OWNER,
- "id": str(self._record.dealerId),
- "share": (Percent(100) - totalP).mongo_amount,
- "money": (totalAmount - totalMoney).mongo_amount
- })
- # TODO 最后再校验一次是否有必要
- logger.info('[_get_partition_map] record = {}, get partition = {}'.format(self._record, partition))
- return partition
- def _record_balance(self, partition):
- """
- 根据收益的划分 对经销商 以及合伙人 收益进行增加
- """
- for _owner in partition:
- _share_money = RMB(_owner['money']) + RMB(_owner.get("elecFee", 0))
- _dealer = Dealer.objects.get(id=_owner["id"])
- self._record_dealer_balance(_dealer, _share_money)
- def _record_dealer_balance(self, dealer, money):
- logger.info("[_record_dealer_balance] dealer = {}, money ={}, record = {}".format(
- dealer, money, self._record
- ))
- if money == RMB(0):
- return
- dealer.record_income(
- source=self._source,
- source_key=self._source_key,
- money=money
- )
|