jiaweiqi 3 years ago
parent
commit
5bb08d1b4e

+ 2 - 2
apps/order/models.py

@@ -53,7 +53,7 @@ class Pay(models.Model):
     def _addnew(amount, customer):
     def _addnew(amount, customer):
         if amount <= 0:
         if amount <= 0:
             raise CustomError(u'无效的付款金额!')
             raise CustomError(u'无效的付款金额!')
-        pay_no = '{}{}'.format(timezone.now().strftime('%y%m%d%H%M%S'), str(customer.id))
+        pay_no = '{}{}'.format(timezone.now().strftime('%Y%m%d%H%M%S'), str(customer.id))
         pay = Pay.objects.create(no=pay_no, status=Pay.WAIT, precreate_amount=amount, customer=customer)
         pay = Pay.objects.create(no=pay_no, status=Pay.WAIT, precreate_amount=amount, customer=customer)
         return pay
         return pay
 
 
@@ -152,7 +152,7 @@ class Order(models.Model):
         生成订单的订单号(注意:该函数最后没有 save 要在调用该函数后save )
         生成订单的订单号(注意:该函数最后没有 save 要在调用该函数后save )
         :return:
         :return:
         '''
         '''
-        no = '{}{}{:0>4}'.format('XCX', timezone.now().strftime('%Y%m%d'), self.id)
+        no = '{}{:0>4}'.format(timezone.now().strftime('%Y%m%d'), self.id)
         self.no = no
         self.no = no
 
 
     def set_order_adderss(self, address_id):
     def set_order_adderss(self, address_id):

+ 228 - 1
apps/rebate/bussiness.py

@@ -1,11 +1,20 @@
 # coding=utf-8
 # coding=utf-8
 
 
+import json
+import datetime
+
 from django.utils import timezone
 from django.utils import timezone
+from django.db.models import Q
 
 
-from apps.rebate.models import CashRebate, PointRebate, CashRebateLog, PointRebateLog, PointGive, PointGiveLog
+from apps.rebate.models import CashRebate, PointRebate, CashRebateLog, PointRebateLog, PointGive, PointGiveLog, TransferCashRebate, TransferCashRebateDetail, CashTransferLog
 from apps.customer.models import SuperiorDistributor
 from apps.customer.models import SuperiorDistributor
 from apps.order.models import Order
 from apps.order.models import Order
 from apps.config.models import Config
 from apps.config.models import Config
+from apps.WechatApplet.models import WechatApplet
+from apps.customer.models import CustomerWechat
+
+from utils.wechatpayv3.transfer import Transfer
+from utils.exceptions import CustomError
 
 
 
 
 class CustomerRebate(object):
 class CustomerRebate(object):
@@ -116,3 +125,221 @@ class CustomerRebate(object):
         if rule > 0:
         if rule > 0:
             amount = int(round((total_amount / 100.0) * (rule / 100.0), 0))
             amount = int(round((total_amount / 100.0) * (rule / 100.0), 0))
             self.point_give(rule, amount, customer)
             self.point_give(rule, amount, customer)
+
+
+class CustomerRebateTransfer(object):
+
+    def __init__(self, order):
+        wx = WechatApplet.objects.filter().first()
+        if not wx:
+            raise Exception(u'小程序appid认证失败!')
+        self.order = order
+        self.wx = wx
+
+    def apply_transfer_again(self, instance):
+        '''转账批次明细失败 重新申请新的批次进行转账  要判断该明细有没有已经申请重新转账 和  判断用户余额'''
+        count = TransferCashRebateDetail.objects.filter(Q(rebate=instance.rebate), ~Q(status=TransferCashRebateDetail.FAIL)).count()
+        if count:
+            raise CustomError(u'该转账明细,正在支付,禁止重复申请!')
+        if instance.customer.balance < instance.amount:
+            raise CustomError(u'申请转账金额超出客户[{}]余额,禁止申请!'.format(instance.customer.name))
+
+        if instance.amount > self.order.total_amount:
+            raise CustomError(u'转账总金额大于收款总金额!')
+        if instance.amount == 0:
+            raise CustomError(u'转账总金额为0!')
+
+        tc = TransferCashRebate.objects.create(order=instance.main.order, transfer_num=1, transfer_amount=instance.amount)
+        tc_no = 'TC{}{:0>4}'.format(timezone.now().strftime('%Y%m%d%H%M%S'), tc.id)
+        tc.no = tc_no
+        tc.save()
+        tcd = TransferCashRebateDetail.objects.create(main=tc, rebate=instance.rebate, amount=instance.amount, customer=instance.customer)
+        tcd_no = 'TCD{}{:0>4}'.format(timezone.now().strftime('%Y%m%d%H%M%S'), tcd.id)
+        tcd.no = tcd_no
+        tcd.save()
+        return tc
+
+    def create_transfer(self):
+        '''
+        转账的时候创建转账批次单 和转账批次单明细
+        如果是没有转账批次的 添加新的转账批次和转账批次明细  如果有转账批次的 只创建失败的
+        要判断收款人的余额
+        如果订单已经申请过转账批次 就不能再申请了 只能在付款失败的批次明细里边重新申请(没有考虑订单关闭的请框)
+        :return:
+        '''
+        count = TransferCashRebate.objects.filter(order=self.order).count()
+        if count:
+            raise CustomError(u'该返利已申请转账,禁止重复申请!')
+        tc = TransferCashRebate.objects.create(order=self.order)
+        tc_no = 'TC{}{:0>4}'.format(timezone.now().strftime('%Y%m%d%H%M%S'), tc.id)
+        rows = CashRebate.objects.filter(order=self.order)
+        transfer_amount = 0
+        transfer_num = 0
+        for row in rows:
+            if row.customer.balance < row.amount:
+                raise CustomError(u'客户[{}]余额不足!'.format(row.customer.name))
+            transfer_num += 1
+            transfer_amount += row.amount
+            tcd = TransferCashRebateDetail.objects.create(main=tc, rebate=row, amount=row.amount, customer=row.customer)
+            tcd_no = 'TCD{}{:0>4}'.format(timezone.now().strftime('%Y%m%d%H%M%S'), tcd.id)
+            tcd.no = tcd_no
+            tcd.save()
+        if transfer_amount > self.order.total_amount:
+            raise CustomError(u'转账总金额大于收款总金额!')
+        if transfer_num == 0:
+            raise CustomError(u'转账总笔数为0!')
+        if transfer_amount == 0:
+            raise CustomError(u'转账总金额为0!')
+        tc.no = tc_no
+        tc.transfer_num = transfer_num
+        tc.transfer_amount = transfer_amount
+        tc.save()
+        return tc
+
+    def transfer_rebate(self, detail=None):
+        '''
+        现金返利转账申请
+        :return:
+        '''
+        if detail:
+            tc = self.apply_transfer_again(detail)
+        else:
+            tc = self.create_transfer()
+
+        transfer_detail_list = []
+        rows = TransferCashRebateDetail.objects.filter(main=tc)
+        for row in rows:
+            wx_c = CustomerWechat.objects.filter(wechat_app=self.wx, customer=row.customer).first()
+            if not wx_c:
+                continue
+            item = {
+                'out_detail_no': row.no,
+                'transfer_amount': row.amount,
+                'transfer_remark': '订单{}返利'.format(self.order.no),
+                'openid': wx_c.openid
+            }
+            # 注意 明细转账金额 大于等于  2000  收款用户姓名必须
+            transfer_detail_list.append(item)
+
+        appid = self.wx.authorizer_appid
+        out_batch_no = self.order.no
+        batch_name = '订单{}返利'.format(self.order.no)
+        batch_remark = '订单{}返利'.format(self.order.no)
+        total_amount = tc.transfer_amount
+        total_num = tc.transfer_num
+
+        t = Transfer(self.wx)
+        code, message = t.transfer_batch(appid, out_batch_no, batch_name, batch_remark, total_amount, total_num, transfer_detail_list)
+        result = json.loads(message)
+        if code != 200:
+            raise Exception(u'[{}]转账申请失败!原因:{}'.format(self.order.no, result))
+
+        batch_id = result.get('batch_id')
+        create_time = result.get('create_time')
+        tc.transfer_status = TransferCashRebate.ACCEPTED
+        if batch_id:
+            tc.batch_id = batch_id
+        if create_time:
+            tc.transfer_time = datetime.datetime.strptime(create_time, "%Y-%m-%dT%H:%M:%S+08:00")
+        tc.save()
+        TransferCashRebateDetail.objects.filter(main=tc).update(status=TransferCashRebateDetail.PENDING)
+
+
+def transfer_refresh(transfer):
+    # 如果转账批次单 已经受理30天了 中间没有刷新  状态是已受理  然后受理时间到当前时间超过30
+    wx = WechatApplet.objects.filter().first()
+    if not wx:
+        raise Exception(u'小程序appid认证失败!')
+    t = Transfer(wx)
+    code, message = t.transfer_query_out_batch_no(transfer.no, need_query_detail=False, offset=0, limit=20, detail_status='ALL')
+    result = json.loads(message)
+    if code != 200:
+        # 如果查询结果  NOT_FOUND   记录不存在 且转账批次单的状态是已申请  把状态改成  未处理  重新申请
+        raise Exception(u'转账[{}]查询失败!原因:{}'.format(transfer.no, result))
+
+    transfer_batch = result.get('transfer_batch')
+    if not transfer_batch:
+        return
+    batch_id = transfer_batch.get('batch_id')
+    batch_status = transfer_batch.get('batch_status')
+    close_reason = transfer_batch.get('close_reason')
+    create_time = transfer_batch.get('create_time')
+    update_time = transfer_batch.get('update_time')
+    success_amount = transfer_batch.get('success_amount')
+    success_num = transfer_batch.get('success_num')
+    fail_amount = transfer_batch.get('fail_amount')
+    fail_num = transfer_batch.get('fail_num')
+
+    if batch_id and not transfer.batch_id:
+        transfer.batch_id = batch_id
+    if batch_status:
+        if batch_status == 'WAIT_PAY':
+            transfer.transfer_status = TransferCashRebate.WAIT_PAY
+        elif batch_status == 'ACCEPTED':
+            transfer.transfer_status = TransferCashRebate.ACCEPTED
+        elif batch_status == 'PROCESSING':
+            transfer.transfer_status = TransferCashRebate.PROCESSING
+        elif batch_status == 'FINISHED':
+            transfer.transfer_status = TransferCashRebate.FINISHED
+        elif batch_status == 'CLOSED':
+            transfer.transfer_status = TransferCashRebate.CLOSED
+    if close_reason:
+        transfer.close_reason = close_reason
+    if create_time and not transfer.transfer_time:
+        transfer.transfer_time = datetime.datetime.strptime(create_time, "%Y-%m-%dT%H:%M:%S+08:00")
+    if update_time:
+        transfer.update_time = datetime.datetime.strptime(update_time, "%Y-%m-%dT%H:%M:%S+08:00")
+    if success_amount:
+        transfer.success_amount = success_amount
+    if success_num:
+        transfer.success_num = success_num
+    if fail_amount:
+        transfer.fail_amount = fail_amount
+    if fail_num:
+        transfer.fail_num = fail_num
+    transfer.save()
+    # 转账结束的时候  自动更新明细信息
+    if batch_status and batch_status == 'FINISHED':
+        detail = TransferCashRebateDetail.objects.filter(main=transfer, status__in=[TransferCashRebateDetail.DEFAULT, TransferCashRebateDetail.PENDING])
+        for item in detail:
+            transfer_detail_refresh(item)
+
+
+def transfer_detail_refresh(detail):
+    wx = WechatApplet.objects.filter().first()
+    if not wx:
+        raise Exception(u'小程序appid认证失败!')
+    out_detail_no = detail.no or None
+    out_batch_no = detail.main and detail.main.no or None
+    t = Transfer(wx)
+    code, message = t.transfer_query_out_detail_no(out_detail_no, out_batch_no)
+    result = json.loads(message)
+    if code != 200:
+        raise Exception(u'转账明细[{}]查询失败!原因:{}'.format(out_detail_no, result))
+    # 更新明细状态
+    detail_id = result.get('detail_id')         # 微信明细单号
+    detail_status = result.get('detail_status') # 明细装填  PROCESSING:转账中。正在处理中,转账结果尚未明确  SUCCESS:转账成功  FAIL:转账失败。需要确认失败原因后,再决定是否重新发起对该笔明细单的转账(并非整个转账批次单)
+    fail_reason = result.get('fail_reason')     # 失败原因
+    initiate_time = result.get('initiate_time') # 转账发起时间
+    update_time = result.get('update_time')     # 明细更新时间
+
+    if detail_id:
+        detail.detail_no = detail_id
+    if detail_status:
+        if detail_status == 'PROCESSING':
+            detail.status = TransferCashRebateDetail.PENDING
+        elif detail_status == 'SUCCESS':
+            detail.status = TransferCashRebateDetail.SUCCESS
+            # 在这将收款人的余额减掉相应的金额
+            # 转账已经成功了 就算是客户的余额是负数  也要统计成负数 转出去的钱不能返回了  所以只有在创建转账明细的时候要控制 转账金额不能大于客户余额
+            CashTransferLog.addnew(detail)
+        elif detail_status == 'FAIL':
+            detail.status = TransferCashRebateDetail.FAIL
+
+    if fail_reason:
+        detail.fail_reason = fail_reason
+    if initiate_time:
+        detail.initiate_time = datetime.datetime.strptime(initiate_time, "%Y-%m-%dT%H:%M:%S+08:00")
+    if update_time:
+        detail.update_time = datetime.datetime.strptime(update_time, "%Y-%m-%dT%H:%M:%S+08:00")
+    detail.save()

+ 14 - 0
apps/rebate/filters.py

@@ -17,3 +17,17 @@ class PointLogFilter(django_filters.FilterSet):
     class Meta:
     class Meta:
         model = PointLog
         model = PointLog
         fields = '__all__'
         fields = '__all__'
+
+
+class TransferCashRebateFilter(django_filters.FilterSet):
+
+    class Meta:
+        model = TransferCashRebate
+        fields = '__all__'
+
+
+class TransferCashRebateDetailFilter(django_filters.FilterSet):
+
+    class Meta:
+        model = TransferCashRebateDetail
+        fields = '__all__'

+ 103 - 2
apps/rebate/models.py

@@ -18,8 +18,10 @@ from utils.exceptions import CustomError
 
 
 class CashLog(models.Model):
 class CashLog(models.Model):
     CASH_REBATE = 1
     CASH_REBATE = 1
+    CASH_TRANSFER = 2
     TYPE_CHOICES = (
     TYPE_CHOICES = (
         (CASH_REBATE, u'现金返利'),
         (CASH_REBATE, u'现金返利'),
+        (CASH_TRANSFER, u'返利提现'),
     )
     )
 
 
     customer = models.ForeignKey(Customer, verbose_name=u'客户', on_delete=models.PROTECT)
     customer = models.ForeignKey(Customer, verbose_name=u'客户', on_delete=models.PROTECT)
@@ -41,8 +43,8 @@ class CashLog(models.Model):
     @staticmethod
     @staticmethod
     def addnew(customer, type, amount):
     def addnew(customer, type, amount):
         customer.balance += amount
         customer.balance += amount
-        if customer.balance < 0:
-            raise CustomError(u'用户余额不足!')
+        # if customer.balance < 0:
+        #     raise CustomError(u'用户余额不足!')
         customer.save()
         customer.save()
         instance = CashLog.objects.create(
         instance = CashLog.objects.create(
             customer=customer,
             customer=customer,
@@ -90,6 +92,105 @@ class CashRebateLog(models.Model):
         instance = CashRebateLog.objects.create(rebate=rebate, log=log)
         instance = CashRebateLog.objects.create(rebate=rebate, log=log)
         return instance
         return instance
 
 
+
+class TransferCashRebate(models.Model):
+    DEFAULT = 0     # 未处理  没有申请转账
+    ACCEPTED = 1    # 已受理。
+    PROCESSING = 2  # 转账中。已开始处理批次内的转账明细单
+    WAIT_PAY = 3    # 待付款  商户员工确认付款阶段
+    FINISHED = 4    # 已完成。批次内的所有转账明细单都处理完成
+    CLOSED = 5      # 已关闭。可查询具体的批次关闭原因
+    STATUS = (
+        (DEFAULT, u'未处理'),
+        (ACCEPTED, u'已受理'),
+        (PROCESSING, u'转账中'),
+        (WAIT_PAY, u'待付款'),
+        (FINISHED, u'已完成'),
+        (CLOSED, u'已关闭'),
+    )
+
+    order = models.ForeignKey(Order, verbose_name=u'订单', related_name='transfer_order', editable=False, on_delete=models.PROTECT)
+    no = models.CharField(max_length=50, verbose_name=u'批次号', editable=False, null=True, blank=True)
+    transfer_status = models.PositiveSmallIntegerField(choices=STATUS, verbose_name=u"返利转账状态", default=DEFAULT)
+    batch_id = models.CharField(verbose_name=u"微信批次单号", max_length=100, null=True, blank=True)
+    transfer_time = models.DateTimeField(verbose_name=u"批次创建时间", null=True, blank=True)
+    transfer_amount = models.BigIntegerField(verbose_name=u'转账总金额', default=0, editable=False)
+    transfer_num = models.IntegerField(verbose_name=u'转账总笔数', default=0, editable=False)
+    success_amount = models.BigIntegerField(verbose_name=u'转账成功金额', default=0, editable=False)
+    success_num = models.IntegerField(verbose_name=u"转账成功笔数", default=0, editable=False)
+    fail_amount = models.BigIntegerField(verbose_name=u"转账失败金额", default=0, editable=False)
+    fail_num = models.IntegerField(verbose_name=u"转账失败笔数", default=0, editable=False)
+    update_time = models.DateTimeField(verbose_name=u"批次更新时间", null=True, blank=True)
+    close_reason = models.CharField(verbose_name=u"批次关闭原因", max_length=100, null=True, blank=True)
+
+    class Meta:
+        db_table = "transfer"
+        verbose_name = u'返利转账'
+        ordering = ['id']
+        index_together = ()
+        unique_together = (
+            'no',
+        )
+        default_permissions = ()
+        permissions = []
+
+
+class TransferCashRebateDetail(models.Model):
+    DEFAULT = 0
+    PENDING = 1
+    SUCCESS = 2
+    FAIL = 3
+    STATUS_CHOICES = (
+        (DEFAULT, u'未处理'),
+        (PENDING, u'转账中'),  # 正在处理中,转账结果尚未明确
+        (SUCCESS, u'转账成功'),
+        (FAIL, u'转账失败'),  # 需要确认失败原因后,在决定是否重新发起对该笔明细单的转账(并非真个转账批次单)  重新发起转账  需要新的批次号重新发起转账
+    )
+
+    no = models.CharField(max_length=50, verbose_name=u'单号', editable=False, null=True, blank=True)
+    main = models.ForeignKey(TransferCashRebate, verbose_name=u'转账', related_name='transfer_detail_main', editable=False, on_delete=models.PROTECT)
+    rebate = models.ForeignKey(CashRebate, verbose_name=u'返利', related_name='transfer_detail_Rebate', editable=False, on_delete=models.PROTECT)
+    amount = models.BigIntegerField(verbose_name=u'返利金额', default=0)
+    customer = models.ForeignKey(Customer, verbose_name=u'客户', editable=False, on_delete=models.PROTECT)
+    create_time = models.DateTimeField(verbose_name=u"创建时间", default=timezone.now, editable=False)
+    detail_no = models.CharField(verbose_name=u"微信明细单号", max_length=100, null=True, blank=True)
+    status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, verbose_name=u"转账状态", default=DEFAULT)
+    fail_reason = models.CharField(max_length=1000, verbose_name=u"转账失败原因", null=True, blank=True)
+    initiate_time = models.DateTimeField(verbose_name=u'转账发起时间', null=True, blank=True)
+    update_time = models.DateTimeField(verbose_name=u'明细更新时间', null=True, blank=True)
+
+    class Meta:
+        db_table = "transfer_detail"
+        verbose_name = u'返利转账明细'
+        ordering = ['id', ]
+        index_together = ()
+        unique_together = (
+            'no',
+        )
+        default_permissions = ()
+        permissions = []
+
+
+class CashTransferLog(models.Model):
+    transfer = models.OneToOneField(TransferCashRebateDetail, verbose_name=u'返利转账', related_name='transfer', editable=False, on_delete=models.PROTECT)
+    log = models.OneToOneField(CashLog, verbose_name=u'日志', related_name='transfer_log', editable=False, on_delete=models.PROTECT)
+
+    class Meta:
+        db_table = "cash_transfer_log"
+        verbose_name = u'现金返利提现日志'
+        ordering = []
+        index_together = ()
+        default_permissions = ()
+        permissions = []
+
+    @staticmethod
+    def addnew(transfer):
+        customer = Customer.objects.select_for_update().filter(id=transfer.customer.id).first()
+        log = CashLog.addnew(customer, CashLog.CASH_TRANSFER, -transfer.amount)
+        instance = CashTransferLog.objects.create(transfer=transfer, log=log)
+        return instance
+
+
 '''
 '''
 积分返利及积分变动记录
 积分返利及积分变动记录
 '''
 '''

+ 106 - 0
apps/rebate/serializers.py

@@ -0,0 +1,106 @@
+# coding=utf-8
+
+import json
+
+from django.conf import settings
+from rest_framework import serializers
+from django.db.models import Q, Sum
+
+from apps.order.models import Order
+from apps.rebate.models import CashRebate, PointRebate, TransferCashRebate, TransferCashRebateDetail
+from apps.base import Formater
+
+
+class OrderRebateSerializer(serializers.ModelSerializer):
+    cash_rebate = serializers.SerializerMethodField()
+    point_rebate = serializers.SerializerMethodField()
+    status_text = serializers.CharField(source='get_status_display', read_only=True)
+    customer_name = serializers.CharField(source='customer.name', read_only=True)
+    customer_tel = serializers.CharField(source='customer.tel', read_only=True)
+    total_amount = serializers.SerializerMethodField()
+    create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M', read_only=True)
+    transfer = serializers.SerializerMethodField()
+
+    class Meta:
+        model = Order
+        fields = '__all__'
+
+    def get_cash_rebate(self, obj):
+        cash_bate = CashRebate.objects.filter(order=obj).aggregate(sum_amount=Sum('amount'))['sum_amount'] or 0
+        return Formater.formatValueShow(cash_bate)
+
+    def get_point_rebate(self, obj):
+        point_rebate = PointRebate.objects.filter(order=obj).aggregate(sum_point=Sum('amount'))['sum_point'] or 0
+        return point_rebate
+
+    def get_total_amount(self, obj):
+        return Formater.formatValueShow(obj.total_amount)
+
+    def get_transfer(self, obj):
+        transfer = TransferCashRebate.objects.filter(order=obj).count()
+        return transfer
+
+
+class CashRebateSerializer(serializers.ModelSerializer):
+    amount = serializers.SerializerMethodField()
+    create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M', read_only=True)
+    customer_name = serializers.CharField(source='customer.name', read_only=True)
+
+    class Meta:
+        model = CashRebate
+        fields = '__all__'
+
+    def get_amount(self, obj):
+        return Formater.formatValueShow(obj.amount)
+
+
+class PointRebateSerializer(serializers.ModelSerializer):
+    create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M', read_only=True)
+    customer_name = serializers.CharField(source='customer.name', read_only=True)
+
+    class Meta:
+        model = PointRebate
+        fields = '__all__'
+
+
+class TransferCashRebateSerializer(serializers.ModelSerializer):
+    transfer_amount = serializers.SerializerMethodField()
+    success_amount = serializers.SerializerMethodField()
+    fail_amount = serializers.SerializerMethodField()
+    transfer_status_text = serializers.CharField(source='get_transfer_status_display', read_only=True)
+    transfer_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M', read_only=True)
+    update_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M', read_only=True)
+
+    class Meta:
+        model = TransferCashRebate
+        fields = '__all__'
+
+    def get_transfer_amount(self, obj):
+        return Formater.formatValueShow(obj.transfer_amount)
+
+    def get_success_amount(self, obj):
+        return Formater.formatValueShow(obj.success_amount)
+
+    def get_fail_amount(self, obj):
+        return Formater.formatValueShow(obj.fail_amount)
+
+
+class TransferCashRebateDetailSerializer(serializers.ModelSerializer):
+    amount = serializers.SerializerMethodField()
+    status_text = serializers.CharField(source='get_status_display', read_only=True)
+    initiate_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M', read_only=True)
+    update_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M', read_only=True)
+    customer_name = serializers.CharField(source='customer.name', read_only=True)
+    tel = serializers.CharField(source='customer.tel', read_only=True)
+    transfer = serializers.SerializerMethodField()
+
+    class Meta:
+        model = TransferCashRebateDetail
+        fields = '__all__'
+
+    def get_amount(self, obj):
+        return Formater.formatValueShow(obj.amount)
+
+    def get_transfer(self, obj):
+        count = TransferCashRebateDetail.objects.filter(Q(rebate=obj.rebate), ~Q(status=TransferCashRebateDetail.FAIL)).count()
+        return count

+ 16 - 0
apps/rebate/urls.py

@@ -0,0 +1,16 @@
+# coding=utf-8
+
+from django.conf.urls import url, include
+from rest_framework.routers import SimpleRouter
+
+from .views import *
+
+urlpatterns = [
+
+]
+
+router = SimpleRouter()
+router.register(r'transfer_detail', TransferCashRebateDetailViewSet)
+router.register(r'transfer', TransferCashRebateViewSet)
+router.register(r'', OrderRebateViewSet)
+urlpatterns += router.urls

+ 148 - 0
apps/rebate/views.py

@@ -0,0 +1,148 @@
+# coding=utf-8
+
+from django.db import transaction
+from django.utils import timezone
+from django.db.models import Q
+
+from rest_framework.decorators import action
+from rest_framework.views import APIView
+from rest_framework import generics
+
+from utils import response_ok
+from utils.permission import IsEmployee
+from utils.exceptions import CustomError
+from utils.custom_modelviewset import CustomModelViewSet
+
+from apps.log.models import BizLog
+from apps.order.models import Order
+from apps.order.filters import OrderFilter
+from apps.rebate.models import PointRebate, CashRebate, TransferCashRebate, TransferCashRebateDetail
+from apps.rebate.filters import TransferCashRebateFilter, TransferCashRebateDetailFilter
+from apps.rebate.serializers import OrderRebateSerializer, CashRebateSerializer, PointRebateSerializer, TransferCashRebateSerializer, TransferCashRebateDetailSerializer
+from apps.rebate.bussiness import CustomerRebateTransfer, transfer_detail_refresh, transfer_refresh
+
+
+class OrderRebateViewSet(CustomModelViewSet):
+    permission_classes = [IsEmployee, ]
+    queryset = Order.objects.filter(status__in=[Order.WAIT_DISPATCH, Order.CONFIRM_DISPATCH])
+    serializer_class = OrderRebateSerializer
+
+    def filter_queryset(self, queryset):
+        order_list = []
+        p_order = PointRebate.objects.filter().values_list('order_id', flat=True)
+        for item in p_order:
+            order_list.append(item)
+        c_order = CashRebate.objects.filter().values_list('order_id', flat=True)
+        for item in c_order:
+            order_list.append(item)
+        order_list = list(set(order_list))
+
+        queryset = queryset.filter(id__in=order_list)
+        f = OrderFilter(self.request.GET, queryset=queryset)
+        return f.qs
+
+    @action(methods=['get'], detail=True)
+    def cash_rebate(self, request, pk):
+        '''
+        现金返利明细
+        :return:
+        '''
+        instance = self.get_object()
+        rows = CashRebate.objects.filter(order=instance)
+        serializer = CashRebateSerializer(rows, many=True)
+        return response_ok(serializer.data)
+
+    @action(methods=['get'], detail=True)
+    def point_rebate(self, request, pk):
+        '''积分返利明细'''
+        instance = self.get_object()
+        rows = PointRebate.objects.filter(order=instance)
+        serializer = PointRebateSerializer(rows, many=True)
+        return response_ok(serializer.data)
+
+    @action(methods=['post'], detail=True)
+    def transfer_rebate(self, request, pk):
+        '''现金返利转账申请'''
+        instance = self.get_object()
+        crt = CustomerRebateTransfer(instance)
+        with transaction.atomic():
+            crt.transfer_rebate()
+            BizLog.objects.addnew(self.request.user, BizLog.INSERT, u'申请订单[{}]现金返利转账'.format(instance.no), )
+        return response_ok()
+
+
+class TransferCashRebateViewSet(CustomModelViewSet):
+    permission_classes = [IsEmployee, ]
+    queryset = TransferCashRebate.objects.filter()
+    serializer_class = TransferCashRebateSerializer
+
+    def filter_queryset(self, queryset):
+        queryset = queryset.filter()
+        f = TransferCashRebateFilter(self.request.GET, queryset=queryset)
+        return f.qs
+
+    def list(self, request, *args, **kwargs):
+        '''查看订单转账记录  同时刷新该转账的状态  刷新这个订单的批次单在处理中的记录 刷新他们的状态 一般一个订单只有一个批次单在处理中'''
+        queryset = self.filter_queryset(self.get_queryset())
+        refresh = queryset.filter(~Q(transfer_status__in=[TransferCashRebate.DEFAULT, TransferCashRebate.FINISHED, TransferCashRebate.CLOSED]))
+        for item in refresh:
+            with transaction.atomic():
+                transfer_refresh(item)
+
+        page = self.paginate_queryset(queryset)
+        if page is not None:
+            serializer = self.get_serializer(page, many=True)
+            return self.get_paginated_response(serializer.data)
+
+        serializer = self.get_serializer(queryset, many=True)
+        return response_ok(serializer.data)
+
+    @action(methods=['post'], detail=True)
+    def apply_again(self, request, pk):
+        instance = self.get_object()
+        if instance.transfer_status != TransferCashRebate.DEFAULT:
+            return
+        # 再次申请转账 但是不需要创建转账表
+        return response_ok()
+
+
+class TransferCashRebateDetailViewSet(CustomModelViewSet):
+    permission_classes = [IsEmployee, ]
+    queryset = TransferCashRebateDetail.objects.filter()
+    serializer_class = TransferCashRebateDetailSerializer
+
+    def filter_queryset(self, queryset):
+        queryset = queryset.filter()
+        f = TransferCashRebateDetailFilter(self.request.GET, queryset=queryset)
+        return f.qs
+
+    def list(self, request, *args, **kwargs):
+        queryset = self.filter_queryset(self.get_queryset())
+
+        refresh = queryset.filter(status=TransferCashRebateDetail.PENDING)
+
+        for item in refresh:
+            # 微信支付明细单号查询明细单
+            with transaction.atomic():
+                transfer_detail_refresh(item)
+
+        page = self.paginate_queryset(queryset)
+        if page is not None:
+            serializer = self.get_serializer(page, many=True)
+            return self.get_paginated_response(serializer.data)
+
+        serializer = self.get_serializer(queryset, many=True)
+        return response_ok(serializer.data)
+
+    @action(methods=['post'], detail=True)
+    def apply_again(self, request, pk):
+        instance = self.get_object()
+        if instance.status != TransferCashRebateDetail.FAIL:
+            raise CustomError(u'该转账明细未失败,禁止再次申请转账!')
+        # 失败的明细重新申请转账  创建新的转账批次和转账批次明细
+        order = instance.main.order
+        crt = CustomerRebateTransfer(order)
+        with transaction.atomic():
+            crt.transfer_rebate(detail=instance)
+            BizLog.objects.addnew(self.request.user, BizLog.INSERT, u'转账明细[{}]转账失败,重新申请转账!'.format(instance.no), )
+        return response_ok()

+ 1 - 0
cosmetics_shop/urls.py

@@ -27,6 +27,7 @@ urlpatterns = [
     url(r'^commodity/', include('apps.commodity.urls')),
     url(r'^commodity/', include('apps.commodity.urls')),
     url(r'^option/', include('apps.option.urls')),
     url(r'^option/', include('apps.option.urls')),
     url(r'^order/', include('apps.order.urls')),
     url(r'^order/', include('apps.order.urls')),
+    url(r'^rebate/', include('apps.rebate.urls')),
     url(r'^api/', include('apps.api.urls')),
     url(r'^api/', include('apps.api.urls')),
 ]
 ]
 
 

+ 1 - 1
requirements

@@ -8,4 +8,4 @@ Pillow
 pycryptodome
 pycryptodome
 requests
 requests
 xmltodict
 xmltodict
-cryptography
+cryptography==36.0.2

+ 3 - 0
uis/views/index.html

@@ -110,6 +110,9 @@
                             <dd data-name="nav">
                             <dd data-name="nav">
                                 <a lay-href="order/index.html">订单管理</a>
                                 <a lay-href="order/index.html">订单管理</a>
                             </dd>
                             </dd>
+                            <dd data-name="nav">
+                                <a lay-href="order_rebate/index.html">返利管理</a>
+                            </dd>
                         </dl>
                         </dl>
                     </li>
                     </li>
                 </ul>
                 </ul>

+ 3 - 3
uis/views/order/index.html

@@ -106,9 +106,9 @@
         {field:'no', title:'订单号',width: 150, fixed: 'left'}
         {field:'no', title:'订单号',width: 150, fixed: 'left'}
        ,{field:'customer_name', title:'下单人',width: 100, fixed: 'left'}
        ,{field:'customer_name', title:'下单人',width: 100, fixed: 'left'}
        ,{field:'customer_tel', title:'电话',width: 120, fixed: 'left'}
        ,{field:'customer_tel', title:'电话',width: 120, fixed: 'left'}
-       ,{field:'total_count', title:'数量',width: 60}
-       ,{field:'total_amount', title:'金额',width: 100}
-       ,{field:'total_point', title:'积分',width: 60}
+       ,{field:'total_count', title:'购买数量',width: 100}
+       ,{field:'total_amount', title:'消费金额',width: 100}
+       ,{field:'total_point', title:'消费积分',width: 100}
        ,{field:'status_text', title:'订单状态',width: 90}
        ,{field:'status_text', title:'订单状态',width: 90}
        ,{field:'create_time', title:'下单时间',width: 150}
        ,{field:'create_time', title:'下单时间',width: 150}
        ,{field:'name', title:'收货人',width: 80}
        ,{field:'name', title:'收货人',width: 80}

+ 134 - 0
uis/views/order_rebate/cash_rebate_detail.html

@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>现金返利明细</title>
+    <meta name="renderer" content="webkit">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport"
+          content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+    <link rel="stylesheet" href="../../layuiadmin/layui/css/layui.css" media="all">
+    <link rel="stylesheet" href="../../layuiadmin/style/admin.css" media="all">
+    <style type="text/css">
+        /*您可以将下列样式写入自己的样式表中*/
+        /*layui 元素样式改写*/
+        .layui-btn-sm {
+            line-height: normal;
+            font-size: 12.5px;
+        }
+
+        .layui-table-view .layui-table-body {
+            min-height: 256px;
+        }
+
+        .layui-table-cell .layui-input.layui-unselect {
+            height: 30px;
+            line-height: 30px;
+        }
+
+        /*设置 layui 表格中单元格内容溢出可见样式*/
+        .table-overlay .layui-table-view,
+        .table-overlay .layui-table-box,
+        .table-overlay .layui-table-body {
+            overflow: visible;
+        }
+
+        .table-overlay .layui-table-cell {
+            height: auto;
+            overflow: visible;
+        }
+
+        .imgStyle {
+            padding-left: 10px;
+            width: 80px;
+            height: 50px;
+        }
+
+        .layui-table-cell {
+            font-size: 14px;
+            padding: 0 5px;
+            height: auto;
+            overflow: visible;
+            text-overflow: inherit;
+            white-space: normal;
+            word-break: break-all;
+        }
+    </style>
+</head>
+<body>
+
+<div class="layui-fluid">
+    <div class="layui-col-md12">
+        <div class="layui-card">
+            <div class="layui-card-body">
+                <div style="height: 5px"></div>
+                <div id="tableRes" class="table-overlay">
+                    <table id="dataTable" lay-filter="dataTable" class="layui-hide"></table>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<script src="../../layuiadmin/layui/layui.js"></script>
+<script>
+    layui.config({
+        base: '../../../layuiadmin/' //静态资源所在路径
+    }).extend({
+        index: 'lib/index' //主入口模块
+    }).use(['index', 'table', 'layer',], function () {
+        var $ = layui.$
+            , admin = layui.admin
+            , table = layui.table
+            , layer = layui.layer;
+
+        var id = layui.view.getParameterByName('id');
+        var editdata = JSON.parse(JSON.stringify(parent.layui.table.editdata)); // 框架有Bug所以这么转换
+        var tbWidth = $("#tableRes").width();
+        var layTableId = "layTable";
+        var tableIns = table.render({
+            elem: '#dataTable',
+            id: layTableId,
+            data: [],
+            width: tbWidth,
+            page: false,
+            limit: 100,
+            loading: true,
+            even: true, //不开启隔行背景
+            cols: [[
+                {title: '序号', type: 'numbers'},
+                {field: 'customer_name', title: '用户名', width: '30%',},
+                {field: 'ratio', title: '返利比例', width: '20%',},
+                {field: 'amount', title: '返利', width: '20%',},
+            ]]
+        });
+
+        admin.req({
+            url: '/rebate/' + id + '/cash_rebate/',
+            done: function (res) {
+                if (res.code === 0) {
+                    var rows = res.data;
+                    var oldData = table.cache[layTableId];
+                    for (var k in rows) {
+                        oldData.push(
+                            {
+                                customer_name: rows[k].customer_name,
+                                ratio: rows[k].ratio + '%',
+                                amount: rows[k].amount
+                            }
+                        )
+                    }
+                    tableIns.reload({
+                        data: oldData
+                    });
+                }else{
+                    layer.msg('获取现金返利明细失败', {icon: 5});
+                }
+            }
+        });
+
+    });
+
+</script>
+</body>
+</html>

+ 209 - 0
uis/views/order_rebate/index.html

@@ -0,0 +1,209 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>返利管理</title>
+  <meta name="renderer" content="webkit">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+  <link rel="stylesheet" href="../../layuiadmin/layui/css/layui.css" media="all">
+  <link rel="stylesheet" href="../../layuiadmin/style/admin.css" media="all">
+  <link rel="stylesheet" type="text/css" href="../../layuiadmin/style/formSelects-v4.css"/>
+    <style type="text/css">
+    .LAY-btns .layui-nav {padding-left:0;padding-right:10px;top:-4px;margin: 0 10px;border: 0;background-color: #009688;}
+    .LAY-btns .layui-nav .layui-nav-item{line-height: 30px;}
+    .LAY-btns .layui-nav .layui-nav-child{top:34px;}
+    .LAY-btns .layui-nav .layui-nav-bar{display: none;}
+    .LAY-btns .layui-nav .layui-nav-child dd.layui-this a{color:#333;background-color:#fff;}
+    .LAY-btns .layui-nav .layui-nav-child dd.layui-this a:hover {background-color: #f2f2f2;color: #000;}
+    .seach_items {float:right;margin-left: 5px;display: inline-block; margin-top: 5px;}
+  </style>
+</head>
+<body>
+
+  <div class="layui-fluid">
+    <div class="layui-card">
+        <div class="layui-card-body" pad15>
+        <div class="layui-row layui-col-space15">
+          <div class="layui-col-md12">
+            <div class="LAY-btns" style="margin-bottom: 10px;">
+              <div class="layui-col-xs12">
+                  <!--<div style="float: left">
+                      <button class="layui-btn" id="btn_download" ><i class="layui-icon layui-icon-download-circle"></i>导出</button>
+                  </div>-->
+                    <form class="layui-form" lay-filter="query-form-element">
+                        <div class="seach_items">
+                            <button class="layui-btn" lay-submit lay-filter="query-form-element"><i class="layui-icon layui-icon-search"></i>查询</button>
+                        </div>
+                        <div class="seach_items">
+                            <div class="layui-inline">
+                              <div class="layui-input-inline">
+                                <input type="text" name="range_time" class="layui-input" id="range_time" placeholder="下单时间">
+                              </div>
+                            </div>
+                        </div>
+                        <div class="seach_items">
+                            <input type="text"  name="customer_tel" autocomplete="off" class="layui-input" placeholder="下单电话"/>
+                        </div>
+                        <div class="seach_items">
+                            <input type="text"  name="no" autocomplete="off" class="layui-input" placeholder="订单号"/>
+                        </div>
+                    </form>
+                </div>
+                <div style="clear: both;"></div>
+            </div>
+            <table class="layui-hide" id="datagrid" lay-filter="datagrid-operate"></table>
+
+            <script type="text/html" id="datagrid-operate-bar">
+                <div class="layui-btn-group">
+                    {{# if(d.transfer == 0 && d.cash_rebate > 0){ }}
+                    <a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="rebate_transfer">转账申请</a>
+                    {{# } }}
+                    {{# if(d.transfer > 0){ }}
+                    <a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="rebate_transfer_log">转账记录</a>
+                    {{# } }}
+                </div>
+            </script>
+            <script type="text/html" id="datagrid-operate-cash-bar">
+                <div class="layui-btn-group">
+                    <a class="layui-btn layui-btn-xs " lay-event="cash_rebate_detail">现金返利明细</a>
+                </div>
+            </script>
+              <script type="text/html" id="datagrid-operate-point-bar">
+                <div class="layui-btn-group">
+                    <a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="point_rebate_detail">积分返利明细</a>
+                </div>
+            </script>
+          </div>
+        </div>
+        </div>
+    </div>
+  </div>
+
+  <script src="../../layuiadmin/layui/layui.js?t=1"></script>
+  <script>
+  var _params = '';
+  layui.config({
+    base: '../../../layuiadmin/' //静态资源所在路径
+  }).extend({
+    index: 'lib/index' //主入口模块
+     ,formSelects: 'formSelects-v4'
+  }).use(['index', 'table', 'form', 'formSelects', 'upload', 'laydate'], function(){
+    var $ = layui.$
+            ,table = layui.table
+            ,laydate = layui.laydate
+            , admin = layui.admin
+            ,form = layui.form;
+    table.render({
+      elem: '#datagrid'
+      ,url: '/rebate/'
+      ,title: '返利信息'
+      ,id: 'datagrid'
+      ,cols: [[
+        {field:'no', title:'订单号',width: 150}
+       ,{field:'customer_name', title:'下单人',width: 100}
+       ,{field:'customer_tel', title:'电话',width: 120}
+       ,{field:'total_count', title:'购买数量',width: 100}
+       ,{field:'total_amount', title:'消费金额',width: 100}
+       ,{field:'total_point', title:'消费积分',width: 100}
+       ,{field:'status_text', title:'订单状态',width: 90}
+       ,{field:'cash_rebate', title:'现金返利',width: 100}
+        ,{width:120, align:'center', toolbar: '#datagrid-operate-cash-bar'}
+       ,{field:'point_rebate', title:'积分返利',width: 100}
+       ,{width:120, align:'center', toolbar: '#datagrid-operate-point-bar'}
+       ,{field:'create_time', title:'下单时间',width: 150}
+        ,{width:150, align:'center', fixed: 'right', toolbar: '#datagrid-operate-bar'}
+      ]]
+      ,totalRow:false
+      , done: function () {
+        layui.index.removeNoPermButtons()
+      }
+      ,page: true
+      ,height: 'full-108'
+    });
+    laydate.render({
+        elem: '#range_time'
+        ,range:true
+    });
+    form.on('submit(query-form-element)', function(data){
+      //layer.msg(JSON.stringify(data.field));
+      table.reload('datagrid', {
+          where: data.field
+          ,page:{curr:1}
+      });
+      _params = data.field;
+      layer.closeAll();
+      return false;
+    });
+    //监听工具条
+    table.on('tool(datagrid-operate)', function(obj){
+      var data = obj.data;
+      table.editdata = data;
+      if(obj.event === 'cash_rebate_detail'){
+        layer.open({
+          type: 2,
+          title: '现金返利明细',
+          shadeClose: false,
+          area: ['60%', '60%'],
+          btn: ['关闭'],
+          yes: function (index, dom) {
+            layer.close(index);//关闭当前按钮
+          },
+          content: 'cash_rebate_detail.html?id='+data.id
+        });
+      } else if (obj.event === 'point_rebate_detail') {
+          layer.open({
+              type: 2,
+              title: '积分返利明细',
+              area: ['60%', '60%'],
+              btn: ['关闭'],
+               yes: function (index, dom) {
+            layer.close(index);//关闭当前按钮
+          },
+          content: 'point_rebate_detail.html?id='+data.id
+          })
+      } else if (obj.event === 'rebate_transfer') {
+        layer.confirm('请核算后再进行转账!', function(index){
+                layer.close(index);
+                layui.admin.req({
+                    notice: true
+                    ,url: '/rebate/' +data.id + '/transfer_rebate/'
+                    ,type: 'post'
+                    ,done: function(res){
+                        table.reload('datagrid',{});
+                    }
+                });
+            });
+      } else if (obj.event === 'rebate_transfer_log') {
+          layer.open({
+              type: 2,
+              title: '转账记录',
+              area: ['95%', '90%'],
+              btn: ['关闭'],
+              yes: function (index, dom) {
+                  layer.close(index);
+              },
+              content: 'transfer_log.html?id=' + data.id
+          })
+      }
+    });
+    // 导出数据
+    $('#btn_download').on('click', function(){
+        $.get({
+            url: '/order/export/',
+            dataType: 'json',
+            data: _params,
+            success: function (res) {
+                if(res.code === 1){
+                    layer.msg(res.msg);
+                    return;
+                }
+                table.exportFile('datagrid', res.data, 'xlsx')
+            }
+        })
+    });
+  });
+  </script>
+</body>
+</html>
+

+ 134 - 0
uis/views/order_rebate/point_rebate_detail.html

@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>积分返利明细</title>
+    <meta name="renderer" content="webkit">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport"
+          content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+    <link rel="stylesheet" href="../../layuiadmin/layui/css/layui.css" media="all">
+    <link rel="stylesheet" href="../../layuiadmin/style/admin.css" media="all">
+    <style type="text/css">
+        /*您可以将下列样式写入自己的样式表中*/
+        /*layui 元素样式改写*/
+        .layui-btn-sm {
+            line-height: normal;
+            font-size: 12.5px;
+        }
+
+        .layui-table-view .layui-table-body {
+            min-height: 256px;
+        }
+
+        .layui-table-cell .layui-input.layui-unselect {
+            height: 30px;
+            line-height: 30px;
+        }
+
+        /*设置 layui 表格中单元格内容溢出可见样式*/
+        .table-overlay .layui-table-view,
+        .table-overlay .layui-table-box,
+        .table-overlay .layui-table-body {
+            overflow: visible;
+        }
+
+        .table-overlay .layui-table-cell {
+            height: auto;
+            overflow: visible;
+        }
+
+        .imgStyle {
+            padding-left: 10px;
+            width: 80px;
+            height: 50px;
+        }
+
+        .layui-table-cell {
+            font-size: 14px;
+            padding: 0 5px;
+            height: auto;
+            overflow: visible;
+            text-overflow: inherit;
+            white-space: normal;
+            word-break: break-all;
+        }
+    </style>
+</head>
+<body>
+
+<div class="layui-fluid">
+    <div class="layui-col-md12">
+        <div class="layui-card">
+            <div class="layui-card-body">
+                <div style="height: 5px"></div>
+                <div id="tableRes" class="table-overlay">
+                    <table id="dataTable" lay-filter="dataTable" class="layui-hide"></table>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<script src="../../layuiadmin/layui/layui.js"></script>
+<script>
+    layui.config({
+        base: '../../../layuiadmin/' //静态资源所在路径
+    }).extend({
+        index: 'lib/index' //主入口模块
+    }).use(['index', 'table', 'layer',], function () {
+        var $ = layui.$
+            , admin = layui.admin
+            , table = layui.table
+            , layer = layui.layer;
+
+        var id = layui.view.getParameterByName('id');
+        var editdata = JSON.parse(JSON.stringify(parent.layui.table.editdata)); // 框架有Bug所以这么转换
+        var tbWidth = $("#tableRes").width();
+        var layTableId = "layTable";
+        var tableIns = table.render({
+            elem: '#dataTable',
+            id: layTableId,
+            data: [],
+            width: tbWidth,
+            page: false,
+            limit: 100,
+            loading: true,
+            even: true, //不开启隔行背景
+            cols: [[
+                {title: '序号', type: 'numbers'},
+                {field: 'customer_name', title: '用户名', width: '30%',},
+                {field: 'ratio', title: '返利比例', width: '20%',},
+                {field: 'amount', title: '返利', width: '20%',},
+            ]]
+        });
+
+        admin.req({
+            url: '/rebate/' + id + '/point_rebate/',
+            done: function (res) {
+                if (res.code === 0) {
+                    var rows = res.data;
+                    var oldData = table.cache[layTableId];
+                    for (var k in rows) {
+                        oldData.push(
+                            {
+                                customer_name: rows[k].customer_name,
+                                ratio: rows[k].ratio + '%',
+                                amount: rows[k].amount
+                            }
+                        )
+                    }
+                    tableIns.reload({
+                        data: oldData
+                    });
+                }else{
+                    layer.msg('获取积分返利明细失败', {icon: 5});
+                }
+            }
+        });
+
+    });
+
+</script>
+</body>
+</html>

+ 96 - 0
uis/views/order_rebate/transfer_log.html

@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>门店管理</title>
+  <meta name="renderer" content="webkit">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+  <link rel="stylesheet" href="../../layuiadmin/layui/css/layui.css" media="all">
+  <link rel="stylesheet" href="../../layuiadmin/style/admin.css" media="all">
+</head>
+<body>
+
+  <div class="layui-fluid">
+    <div class="layui-card">
+        <div class="layui-card-body" pad15>
+        <div class="layui-row layui-col-space15">
+          <div class="layui-col-md12">
+
+            <table class="layui-hide" id="datagrid-log" lay-filter="datagrid-operate"></table>
+
+            <script type="text/html" id="datagrid-operate-bar">
+                <div class="layui-btn-group">
+                    {{# if(d.transfer_status >= 1){ }}
+                    <a class="layui-btn layui-btn-danger layui-btn-xs"  lay-event="detail">明细</a>
+                    {{# } }}
+                </div>
+            </script>
+          </div>
+        </div>
+        </div>
+    </div>
+  </div>
+
+  <script src="../../layuiadmin/layui/layui.js?t=1"></script>
+  <script>
+  var _params = '';
+  layui.config({
+    base: '../../../layuiadmin/' //静态资源所在路径
+  }).extend({
+    index: 'lib/index' //主入口模块
+  }).use(['index', 'table'], function(){
+    var $ = layui.$
+    ,form = layui.form;
+    var table = layui.table;
+    var id = layui.view.getParameterByName('id');
+
+    table.render({
+      elem: '#datagrid-log'
+      ,url: '/rebate/transfer/?order=' + id
+      ,where: {}
+      ,cols: [[
+        {title: '序号', type: 'numbers'},
+        {field: 'no', title: '商家批次号', width: '200',},
+        //{field: 'batch_id', title: '微信批次号', width: '10%',},
+        {field: 'transfer_amount', title: '转账金额', width: '100',},
+        {field: 'transfer_num', title: '转账笔数', width: '100',},
+        {field: 'success_amount', title: '成功金额', width: '100',},
+        {field: 'success_num', title: '成功笔数', width: '100',},
+        {field: 'fail_amount', title: '失败金额', width: '100',},
+        {field: 'fail_num', title: '失败笔数', width: '100',},
+        {field: 'transfer_status_text', title: '状态', width: '80',},
+        {field: 'close_reason', title: '关闭原因', width: '150',},
+        {field: 'transfer_time', title: '创建时间', width: '150',},
+        {field: 'update_time', title: '更新时间', width: '150',},
+        {width:100, align:'center', fixed: 'right', toolbar: '#datagrid-operate-bar'}
+      ]]
+      ,page: true
+      ,height: 'full-104'
+      ,done: function () {
+        layui.index.removeNoPermButtons();
+      }
+    });
+
+    //监听工具条
+    table.on('tool(datagrid-operate)', function(obj){
+        var data = obj.data;
+        if(obj.event === 'detail'){
+            layer.open({
+              type: 2,
+              title: '转账记录明细',
+              area: ['95%', '90%'],
+              btn: ['关闭'],
+              yes: function (index, dom) {
+                  layer.close(index);
+              },
+              content: 'transfer_log_detail.html?id=' + data.id
+          })
+          }
+    });
+
+  });
+  </script>
+</body>
+</html>
+

+ 94 - 0
uis/views/order_rebate/transfer_log_detail.html

@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>门店管理</title>
+  <meta name="renderer" content="webkit">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
+  <link rel="stylesheet" href="../../layuiadmin/layui/css/layui.css" media="all">
+  <link rel="stylesheet" href="../../layuiadmin/style/admin.css" media="all">
+</head>
+<body>
+
+  <div class="layui-fluid">
+    <div class="layui-card">
+        <div class="layui-card-body" pad15>
+        <div class="layui-row layui-col-space15">
+          <div class="layui-col-md12">
+
+            <table class="layui-hide" id="datagrid" lay-filter="datagrid-operate"></table>
+
+            <script type="text/html" id="datagrid-operate-bar">
+                <div class="layui-btn-group">
+                    {{# if(d.status == 3 && d.transfer == 0){ }}
+                  <a class="layui-btn layui-btn-danger layui-btn-xs"  lay-event="detail_apply">再次申请</a>
+                    {{# } }}
+                </div>
+            </script>
+          </div>
+        </div>
+        </div>
+    </div>
+  </div>
+
+  <script src="../../layuiadmin/layui/layui.js?t=1"></script>
+  <script>
+  var _params = '';
+  layui.config({
+    base: '../../../layuiadmin/' //静态资源所在路径
+  }).extend({
+    index: 'lib/index' //主入口模块
+  }).use(['index', 'table'], function(){
+    var $ = layui.$
+    ,form = layui.form;
+    var table = layui.table;
+    var id = layui.view.getParameterByName('id');
+
+    table.render({
+      elem: '#datagrid'
+      ,url: '/rebate/transfer_detail/?main=' + id
+      ,where: {}
+      ,cols: [[
+        {title: '序号', type: 'numbers'},
+        {field: 'no', title: '商家批次明细号', width: '200',},
+        //{field: 'batch_id', title: '微信批次号', width: '10%',},
+        {field: 'customer_name', title: '收款人', width: '90',},
+        {field: 'tel', title: '收款电话', width: '120',},
+        {field: 'amount', title: '转账金额', width: '100',},
+        {field: 'status_text', title: '状态', width: '80',},
+        {field: 'fail_reason', title: '失败原因', width: '150',},
+        {field: 'initiate_time', title: '转账发起时间', width: '150',},
+        {field: 'update_time', title: '更新时间', width: '150',},
+        {width:100, align:'center', fixed: 'right', toolbar: '#datagrid-operate-bar'}
+      ]]
+      ,page: true
+      ,height: 'full-104'
+      ,done: function () {
+        layui.index.removeNoPermButtons();
+      }
+    });
+
+    //监听工具条
+    table.on('tool(datagrid-operate)', function(obj){
+        var data = obj.data;
+        if(obj.event === 'detail_apply'){
+            layer.confirm('确定要重新申请该笔转账么?', function(index){
+                layer.close(index);
+                layui.admin.req({
+                    notice: true
+                    ,url: '/rebate/transfer_detail/' + data.id + '/apply_again/'
+                    ,type: 'post'
+                    ,done: function(res){
+                        table.reload('datagrid',{});
+                    }
+                });
+            });
+        }
+    });
+
+  });
+  </script>
+</body>
+</html>
+

+ 65 - 6
utils/wechatpayv3/transfer.py

@@ -3,14 +3,9 @@
 from .type import RequestType
 from .type import RequestType
 from .core import Core
 from .core import Core
 
 
-from apps.WechatApplet.models import WechatApplet
-
 
 
 class Transfer(object):
 class Transfer(object):
-    def __init__(self, appid):
-        wx = WechatApplet.objects.filter(authorizer_appid=appid).first()
-        if not wx:
-            raise Exception(u'小程序appid认证失败!')
+    def __init__(self, wx):
         self._core = Core(wx.agent_num, wx.cert_serial_no, wx.apiv3_key)
         self._core = Core(wx.agent_num, wx.cert_serial_no, wx.apiv3_key)
 
 
     def transfer_batch(self, appid, out_batch_no, batch_name, batch_remark, total_amount, total_num, transfer_detail_list=[]):
     def transfer_batch(self, appid, out_batch_no, batch_name, batch_remark, total_amount, total_num, transfer_detail_list=[]):
@@ -64,3 +59,67 @@ class Transfer(object):
         }
         }
         path = '/v3/transfer/batches'
         path = '/v3/transfer/batches'
         return self._core.request(path, method=RequestType.POST, data=params, cipher_data=False)
         return self._core.request(path, method=RequestType.POST, data=params, cipher_data=False)
+
+    def transfer_query_batchid(self, batch_id, need_query_detail=False, offset=0, limit=20, detail_status='ALL'):
+        """微信批次单号查询批次单
+        :param batch_id: 微信批次单号,微信商家转账系统返回的唯一标识,示例值:1030000071100999991182020050700019480001
+        :param need_query_detail: 是否查询转账明细单,枚举值:true:是;false:否,默认否。
+        :param offset: 请求资源起始位置,默认值为0
+        :param limit: 最大资源条数,默认值为20
+        :param detail_status: 明细状态, ALL:全部。需要同时查询转账成功和转账失败的明细单;SUCCESS:转账成功。只查询转账成功的明细单;FAIL:转账失败。
+        """
+        if batch_id:
+            path = '/v3/transfer/batches/batch-id/%s' % batch_id
+        else:
+            raise Exception('参数错误!')
+        if need_query_detail:
+            path += '?need_query_detail=true'
+        else:
+            path += '?need_query_detail=false'
+        path += '&offset=%s' % offset
+        path += '&limit=%s' % limit
+        path += '&detail_status=%s' % detail_status
+        return self._core.request(path)
+
+    def transfer_query_detail_id(self, batch_id, detail_id):
+        """微信明细单号查询明细单
+        :param batch_id: 微信批次单号,微信商家转账系统返回的唯一标识,示例值:1030000071100999991182020050700019480001
+        :param detail_id: 微信明细单号,微信支付系统内部区分转账批次单下不同转账明细单的唯一标识,示例值:1040000071100999991182020050700019500100
+        """
+        if batch_id and detail_id:
+            path = '/v3/transfer/batches/batch-id/%s/details/detail-id/%s' % (batch_id, detail_id)
+        else:
+            raise Exception('参数错误!')
+        return self._core.request(path)
+
+    def transfer_query_out_batch_no(self, out_batch_no, need_query_detail=False, offset=0, limit=20, detail_status='ALL'):
+        """商家批次单号查询批次单
+        :param out_batch_no: 商家批次单号,示例值:plfk2020042013
+        :param need_query_detail: 是否查询转账明细单,枚举值:true:是;false:否,默认否。
+        :param offset: 请求资源起始位置,默认值为0
+        :param limit: 最大资源条数,默认值为20
+        :param detail_status: 明细状态, ALL:全部。需要同时查询转账成功和转账失败的明细单;SUCCESS:转账成功。只查询转账成功的明细单;FAIL:转账失败。
+        """
+        if out_batch_no:
+            path = '/v3/transfer/batches/out-batch-no/%s' % out_batch_no
+        else:
+            raise Exception('参数错误!')
+        if need_query_detail:
+            path += '?need_query_detail=true'
+        else:
+            path += '?need_query_detail=false'
+        path += '&offset=%s' % offset
+        path += '&limit=%s' % limit
+        path += '&detail_status=%s' % detail_status
+        return self._core.request(path)
+
+    def transfer_query_out_detail_no(self, out_detail_no, out_batch_no):
+        """商家明细单号查询明细单
+        :param out_detail_no: 商家明细单号,示例值:x23zy545Bd5436
+        :param out_batch_no: 商家批次单号,示例值:plfk2020042013
+        """
+        if out_detail_no and out_batch_no:
+            path = '/v3/transfer/batches/out-batch-no/{}/details/out-detail-no/{}'.format(out_batch_no, out_detail_no)
+        else:
+            raise Exception('参数错误!')
+        return self._core.request(path)