jiaweiqi 2 年 前
コミット
5bb08d1b4e

+ 2 - 2
apps/order/models.py

@@ -53,7 +53,7 @@ class Pay(models.Model):
     def _addnew(amount, customer):
         if amount <= 0:
             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)
         return pay
 
@@ -152,7 +152,7 @@ class Order(models.Model):
         生成订单的订单号(注意:该函数最后没有 save 要在调用该函数后save )
         :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
 
     def set_order_adderss(self, address_id):

+ 228 - 1
apps/rebate/bussiness.py

@@ -1,11 +1,20 @@
 # coding=utf-8
 
+import json
+import datetime
+
 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.order.models import Order
 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):
@@ -116,3 +125,221 @@ class CustomerRebate(object):
         if rule > 0:
             amount = int(round((total_amount / 100.0) * (rule / 100.0), 0))
             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:
         model = PointLog
         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):
     CASH_REBATE = 1
+    CASH_TRANSFER = 2
     TYPE_CHOICES = (
         (CASH_REBATE, u'现金返利'),
+        (CASH_TRANSFER, u'返利提现'),
     )
 
     customer = models.ForeignKey(Customer, verbose_name=u'客户', on_delete=models.PROTECT)
@@ -41,8 +43,8 @@ class CashLog(models.Model):
     @staticmethod
     def addnew(customer, type, amount):
         customer.balance += amount
-        if customer.balance < 0:
-            raise CustomError(u'用户余额不足!')
+        # if customer.balance < 0:
+        #     raise CustomError(u'用户余额不足!')
         customer.save()
         instance = CashLog.objects.create(
             customer=customer,
@@ -90,6 +92,105 @@ class CashRebateLog(models.Model):
         instance = CashRebateLog.objects.create(rebate=rebate, log=log)
         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'^option/', include('apps.option.urls')),
     url(r'^order/', include('apps.order.urls')),
+    url(r'^rebate/', include('apps.rebate.urls')),
     url(r'^api/', include('apps.api.urls')),
 ]
 

+ 1 - 1
requirements

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

+ 3 - 0
uis/views/index.html

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

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

@@ -106,9 +106,9 @@
         {field:'no', title:'订单号',width: 150, fixed: 'left'}
        ,{field:'customer_name', title:'下单人',width: 100, 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:'create_time', title:'下单时间',width: 150}
        ,{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 .core import Core
 
-from apps.WechatApplet.models import WechatApplet
-
 
 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)
 
     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'
         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)