作者 vanwhebin

增加产品SKU审批

1 -品牌四部企业微信对接项目  
  1 +### 品牌四部企业微信对接项目
  2 +
  3 +![](https://img.shields.io/badge/Python-v3.8-brightgreen)
  4 +
  5 +[gitlab地址](http://gitlab.aukeyit.com/wanweibin/product-project.git)
@@ -13,7 +13,6 @@ from .models import Auditor, Project, Result @@ -13,7 +13,6 @@ from .models import Auditor, Project, Result
13 from utils.helpers import WxPushHelper 13 from utils.helpers import WxPushHelper
14 from utils.pagination import MyPageNumberPagination 14 from utils.pagination import MyPageNumberPagination
15 from utils.util import response 15 from utils.util import response
16 -from wxProject.settings import FRONT_URL  
17 from wxProject.qywx_settings import Conf, project_conf 16 from wxProject.qywx_settings import Conf, project_conf
18 17
19 18
@@ -37,8 +36,8 @@ class CreateProject(CreateAPIView): @@ -37,8 +36,8 @@ class CreateProject(CreateAPIView):
37 auditor_id=i.pk, 36 auditor_id=i.pk,
38 project_id=serializer.data['id'] 37 project_id=serializer.data['id']
39 ) 38 )
40 - url = re.sub("PK", str(obj_dict['id']), FRONT_URL['flow_detail'])  
41 - url = re.sub("REDIRECT_URL", parse.quote(url, safe=''), FRONT_URL['wx_authorize']) 39 + url = re.sub("PK", str(obj_dict['id']), project_conf['flow_detail'])
  40 + url = re.sub("REDIRECT_URL", parse.quote(url, safe=''), project_conf['wx_authorize'])
42 first_auditor = auditors[0] 41 first_auditor = auditors[0]
43 42
44 wx_client.push_card(first_auditor.user.wx_token, url, f"{request.user.username}提交了一个产品立项申请") 43 wx_client.push_card(first_auditor.user.wx_token, url, f"{request.user.username}提交了一个产品立项申请")
@@ -101,8 +100,8 @@ class AuditProject(UpdateAPIView): @@ -101,8 +100,8 @@ class AuditProject(UpdateAPIView):
101 100
102 wx_client = WxPushHelper(Conf[project_conf['APP_ID']]) 101 wx_client = WxPushHelper(Conf[project_conf['APP_ID']])
103 full_audit_done = self._check_audit(obj) 102 full_audit_done = self._check_audit(obj)
104 - url = re.sub("PK", str(obj.id), FRONT_URL['flow_detail'])  
105 - url = re.sub("REDIRECT_URL", parse.quote(url, safe=''), FRONT_URL['wx_authorize']) 103 + url = re.sub("PK", str(obj.id), project_conf['flow_detail'])
  104 + url = re.sub("REDIRECT_URL", parse.quote(url, safe=''), project_conf['wx_authorize'])
106 desc = "产品立项流程所有审批已完成" if full_audit_done else f"{request.user.username}已审批完成" 105 desc = "产品立项流程所有审批已完成" if full_audit_done else f"{request.user.username}已审批完成"
107 if full_audit_done: 106 if full_audit_done:
108 obj.is_done = True 107 obj.is_done = True
不能预览此文件类型
不能预览此文件类型
  1 +# Generated by Django 3.1.1 on 2020-11-03 08:15
  2 +
  3 +from django.conf import settings
  4 +from django.db import migrations, models
  5 +import django.db.models.deletion
  6 +
  7 +
  8 +class Migration(migrations.Migration):
  9 +
  10 + initial = True
  11 +
  12 + dependencies = [
  13 + migrations.swappable_dependency(settings.AUTH_USER_MODEL),
  14 + ]
  15 +
  16 + operations = [
  17 + migrations.CreateModel(
  18 + name='Auditor',
  19 + fields=[
  20 + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
  21 + ('order', models.PositiveSmallIntegerField(blank=True, default=0, null=True, verbose_name='排序')),
  22 + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sku_aud', to=settings.AUTH_USER_MODEL, verbose_name='审批人员')),
  23 + ],
  24 + options={
  25 + 'ordering': ('order',),
  26 + },
  27 + ),
  28 + migrations.CreateModel(
  29 + name='Flow',
  30 + fields=[
  31 + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
  32 + ('is_done', models.BooleanField(default=False, verbose_name='是否已结束')),
  33 + ('create_time', models.DateTimeField(auto_now_add=True)),
  34 + ('auditor', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='sku.auditor', verbose_name='审核人员')),
  35 + ('starter', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL, verbose_name='发起人')),
  36 + ],
  37 + ),
  38 + migrations.CreateModel(
  39 + name='SKU',
  40 + fields=[
  41 + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
  42 + ('supplier', models.CharField(default='', max_length=100, verbose_name='供应商')),
  43 + ('sku', models.CharField(default='', max_length=100, verbose_name='SKU')),
  44 + ('model', models.CharField(default='', max_length=100, verbose_name='型号')),
  45 + ('title', models.CharField(default='', max_length=100, verbose_name='SKU名称')),
  46 + ('is_new', models.CharField(default='', max_length=100, verbose_name='是否新品')),
  47 + ('qty', models.CharField(default='', max_length=100, verbose_name='需求单数量')),
  48 + ('price_with_tax', models.DecimalField(decimal_places=4, default=0, max_digits=9, verbose_name='含税单价')),
  49 + ('amount_with_tax', models.DecimalField(decimal_places=4, default=0, max_digits=9, verbose_name='含税总金额')),
  50 + ('sell_day', models.PositiveIntegerField(default=0, null=True, verbose_name='可售天数')),
  51 + ('qty_within_30', models.PositiveIntegerField(default=0, null=True, verbose_name='30天销量')),
  52 + ('inventory', models.PositiveIntegerField(default=0, null=True, verbose_name='库存')),
  53 + ('coming_inventory', models.PositiveIntegerField(default=0, null=True, verbose_name='采购在途')),
  54 + ('gross_profit_rate', models.FloatField(default=0, null=True, verbose_name='毛利率')),
  55 + ('return_rate', models.FloatField(default=0, null=True, verbose_name='客退率')),
  56 + ('memo', models.CharField(default='', max_length=255, null=True, verbose_name='备注')),
  57 + ('create_time', models.DateTimeField(auto_now_add=True)),
  58 + ('purchaser', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='purchase_user', to=settings.AUTH_USER_MODEL, verbose_name='采购')),
  59 + ],
  60 + ),
  61 + migrations.CreateModel(
  62 + name='Result',
  63 + fields=[
  64 + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
  65 + ('is_accept', models.CharField(choices=[('accept', '通过'), ('reject', '否决')], max_length=10, null=True, verbose_name='审核项目')),
  66 + ('memo', models.CharField(blank=True, default='', max_length=300, verbose_name='审核结果陈述')),
  67 + ('create_time', models.DateTimeField(auto_now_add=True)),
  68 + ('auditor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='result_auditor', to='sku.auditor', verbose_name='审核人员')),
  69 + ('flow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='result_flow', to='sku.flow', verbose_name='审核项目')),
  70 + ],
  71 + options={
  72 + 'ordering': ('pk',),
  73 + },
  74 + ),
  75 + migrations.CreateModel(
  76 + name='Leader',
  77 + fields=[
  78 + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
  79 + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sku_leader', to=settings.AUTH_USER_MODEL, verbose_name='采购负责人')),
  80 + ],
  81 + ),
  82 + migrations.CreateModel(
  83 + name='FlowSKU',
  84 + fields=[
  85 + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
  86 + ('is_pass', models.BooleanField(default=False, verbose_name='是否通过,默认为否')),
  87 + ('flow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sku.flow', verbose_name='对应的流程')),
  88 + ('sku', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sku.sku', verbose_name='流程中的的SKU')),
  89 + ],
  90 + ),
  91 + ]
  1 +# Generated by Django 3.1.1 on 2020-11-03 09:51
  2 +
  3 +from django.db import migrations, models
  4 +
  5 +
  6 +class Migration(migrations.Migration):
  7 +
  8 + dependencies = [
  9 + ('sku', '0001_initial'),
  10 + ]
  11 +
  12 + operations = [
  13 + migrations.AlterField(
  14 + model_name='sku',
  15 + name='is_new',
  16 + field=models.CharField(choices=[('是', '新品'), ('否', '非新品')], max_length=10, verbose_name='是否新品'),
  17 + ),
  18 + migrations.AlterField(
  19 + model_name='sku',
  20 + name='purchaser',
  21 + field=models.CharField(max_length=255, verbose_name='采购名字'),
  22 + ),
  23 + migrations.AlterField(
  24 + model_name='sku',
  25 + name='supplier',
  26 + field=models.CharField(default='', max_length=255, verbose_name='供应商'),
  27 + ),
  28 + ]
  1 +# Generated by Django 3.1.1 on 2020-11-03 10:18
  2 +
  3 +from django.db import migrations, models
  4 +
  5 +
  6 +class Migration(migrations.Migration):
  7 +
  8 + dependencies = [
  9 + ('sku', '0002_auto_20201103_1751'),
  10 + ]
  11 +
  12 + operations = [
  13 + migrations.AlterField(
  14 + model_name='sku',
  15 + name='amount_with_tax',
  16 + field=models.DecimalField(decimal_places=4, default=0, max_digits=12, verbose_name='含税总金额'),
  17 + ),
  18 + migrations.AlterField(
  19 + model_name='sku',
  20 + name='price_with_tax',
  21 + field=models.DecimalField(decimal_places=4, default=0, max_digits=12, verbose_name='含税单价'),
  22 + ),
  23 + ]
  1 +# Generated by Django 3.1.1 on 2020-11-03 10:57
  2 +
  3 +from django.db import migrations
  4 +
  5 +
  6 +class Migration(migrations.Migration):
  7 +
  8 + dependencies = [
  9 + ('sku', '0003_auto_20201103_1818'),
  10 + ]
  11 +
  12 + operations = [
  13 + migrations.RemoveField(
  14 + model_name='flow',
  15 + name='auditor',
  16 + ),
  17 + ]
  1 +# Generated by Django 3.1.1 on 2020-11-05 09:10
  2 +
  3 +from django.db import migrations, models
  4 +import django.db.models.deletion
  5 +
  6 +
  7 +class Migration(migrations.Migration):
  8 +
  9 + dependencies = [
  10 + ('sku', '0004_remove_flow_auditor'),
  11 + ]
  12 +
  13 + operations = [
  14 + migrations.AlterField(
  15 + model_name='flowsku',
  16 + name='flow',
  17 + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sku_flow', to='sku.flow', verbose_name='对应的流程'),
  18 + ),
  19 + migrations.AlterField(
  20 + model_name='flowsku',
  21 + name='sku',
  22 + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='flow_sku', to='sku.sku', verbose_name='流程中的的SKU'),
  23 + ),
  24 + migrations.AlterField(
  25 + model_name='sku',
  26 + name='memo',
  27 + field=models.CharField(blank=True, default='', max_length=255, null=True, verbose_name='备注'),
  28 + ),
  29 + ]
  1 +# Generated by Django 3.1.1 on 2020-11-05 09:39
  2 +
  3 +from django.db import migrations, models
  4 +
  5 +
  6 +class Migration(migrations.Migration):
  7 +
  8 + dependencies = [
  9 + ('sku', '0005_auto_20201105_1710'),
  10 + ]
  11 +
  12 + operations = [
  13 + migrations.AlterField(
  14 + model_name='flowsku',
  15 + name='is_pass',
  16 + field=models.BooleanField(null=True, verbose_name='是否通过,默认为否'),
  17 + ),
  18 + ]
1 from django.db import models 1 from django.db import models
2 2
3 -# Create your models here. 3 +
  4 +from usercenter.models import User
  5 +
  6 +
  7 +class Auditor(models.Model):
  8 + user = models.ForeignKey(
  9 + User,
  10 + on_delete=models.CASCADE,
  11 + related_name="sku_aud",
  12 + verbose_name="审批人员")
  13 + order = models.PositiveSmallIntegerField(default=0, null=True, blank=True, verbose_name="排序")
  14 +
  15 + def __str__(self):
  16 + return self.user.username
  17 +
  18 + class Meta:
  19 + ordering = ('order', )
  20 +
  21 +
  22 +class SKU(models.Model):
  23 + NEW_CHOICES = (
  24 + ("是", "新品"),
  25 + ("否", "非新品")
  26 + )
  27 + supplier = models.CharField(max_length=255, default="", verbose_name="供应商")
  28 + sku = models.CharField(max_length=100, default="", verbose_name="SKU")
  29 + model = models.CharField(max_length=100, default="", verbose_name="型号")
  30 + title = models.CharField(max_length=100, default="", verbose_name="SKU名称")
  31 + is_new = models.CharField(max_length=10, choices=NEW_CHOICES, verbose_name="是否新品")
  32 + qty = models.CharField(max_length=100, default="", verbose_name="需求单数量")
  33 + price_with_tax = models.DecimalField(max_digits=12, decimal_places=4, default=0, verbose_name="含税单价")
  34 + amount_with_tax = models.DecimalField(max_digits=12, decimal_places=4, default=0, verbose_name="含税总金额")
  35 + sell_day = models.PositiveIntegerField(default=0, null=True, verbose_name="可售天数")
  36 + qty_within_30 = models.PositiveIntegerField(default=0, null=True, verbose_name="30天销量")
  37 + inventory = models.PositiveIntegerField(default=0, null=True, verbose_name="库存")
  38 + coming_inventory = models.PositiveIntegerField(default=0, null=True, verbose_name="采购在途")
  39 + gross_profit_rate = models.FloatField(default=0, null=True, verbose_name="毛利率")
  40 + return_rate = models.FloatField(default=0, null=True, verbose_name="客退率")
  41 + memo = models.CharField(default="", max_length=255, null=True, verbose_name="备注", blank=True)
  42 + create_time = models.DateTimeField(auto_now_add=True)
  43 + purchaser = models.CharField(max_length=255, verbose_name="采购名字")
  44 +
  45 +
  46 +class Flow(models.Model):
  47 + """ 提交发起的流程"""
  48 + starter = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="发起人")
  49 + is_done = models.BooleanField(default=False, verbose_name="是否已结束")
  50 + create_time = models.DateTimeField(auto_now_add=True)
  51 +
  52 +
  53 +class FlowSKU(models.Model):
  54 + """ 流程中的SKU """
  55 + sku = models.ForeignKey(SKU, on_delete=models.CASCADE, verbose_name="流程中的的SKU", related_name="flow_sku")
  56 + flow = models.ForeignKey(Flow, on_delete=models.CASCADE, verbose_name="对应的流程", related_name="sku_flow")
  57 + is_pass = models.BooleanField(null=True, verbose_name="是否通过,默认为否")
  58 +
  59 +
  60 +class Result(models.Model):
  61 + ACCEPT_CHOICES = (
  62 + ('accept', '通过'),
  63 + ('reject', '否决')
  64 + )
  65 +
  66 + auditor = models.ForeignKey(Auditor, on_delete=models.CASCADE, related_name="result_auditor", verbose_name="审核人员")
  67 + flow = models.ForeignKey(Flow, on_delete=models.CASCADE, related_name="result_flow", verbose_name="审核项目")
  68 + is_accept = models.CharField(max_length=10, null=True, choices=ACCEPT_CHOICES, verbose_name="审核项目")
  69 + memo = models.CharField(max_length=300, blank=True, default="", verbose_name="审核结果陈述")
  70 + create_time = models.DateTimeField(auto_now_add=True)
  71 +
  72 + class Meta:
  73 + ordering = ('pk', )
  74 +
  75 +
  76 +class Leader(models.Model):
  77 + """ 采购负责人"""
  78 + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sku_leader", verbose_name="采购负责人")
  79 +
  80 +
  1 +# _*_ coding: utf-8 _*_
  2 +# @Time : 2020/11/3 16:34
  3 +# @Author vanwhebin
  4 +
  5 +from rest_framework import serializers
  6 +
  7 +from .models import Auditor, Flow, Result, SKU, FlowSKU
  8 +
  9 +
  10 +class AuditorSerializer(serializers.ModelSerializer):
  11 +
  12 + class Meta:
  13 + model = Auditor
  14 + fields = '__all__'
  15 + depth = 1
  16 +
  17 +
  18 +class SKUSerializer(serializers.ModelSerializer):
  19 + # pk = serializers.ReadOnlyField()
  20 + # is_new = serializers.ReadOnlyField()
  21 + # purchaser = serializers.ReadOnlyField()
  22 +
  23 + class Meta:
  24 + model = SKU
  25 + # fields = ("supplier", "sku", "model", "title", "is_new", "qty", "price_with_tax", "amount_with_tax", "sell_day",
  26 + # "qty_within_30", "inventory", "coming_inventory", "gross_profit_rate", "return_rate", "memo",
  27 + # "purchaser")
  28 + fields = "__all__"
  29 +
  30 +
  31 +class FlowSerializer(serializers.ModelSerializer):
  32 + result = serializers.ReadOnlyField()
  33 + creator_name = serializers.ReadOnlyField()
  34 + skus = serializers.ReadOnlyField()
  35 +
  36 + class Meta:
  37 + model = Flow
  38 + fields = ("result", "creator_name", "skus", "is_done", "create_time")
  39 +
  40 +
  41 +class FlowListSerializer(serializers.ModelSerializer):
  42 + result = serializers.BooleanField()
  43 + creator_name = serializers.CharField()
  44 + flow_id = serializers.IntegerField()
  45 +
  46 + class Meta:
  47 + model = Result
  48 + fields = ("creator_name", "result", "flow_id", "create_time")
  49 +
  50 +
  51 +class ResultSerializer(serializers.ModelSerializer):
  52 +
  53 + class Meta:
  54 + model = Result
  55 + fields = "__all__"
  56 +
  57 +
  58 +class FlowSKUSerializer(serializers.ModelSerializer):
  59 +
  60 + class Meta:
  61 + model = FlowSKU
  62 + fields = "__all__"
  63 + depth = 1
  64 +
@@ -2,10 +2,17 @@ @@ -2,10 +2,17 @@
2 # @Time : 2020/11/2 18:41 2 # @Time : 2020/11/2 18:41
3 # @Author vanwhebin 3 # @Author vanwhebin
4 4
5 -from django.urls import path, include 5 +from django.urls import path
  6 +from sku import views
6 7
7 app_name = "sku" 8 app_name = "sku"
8 9
9 urlpatterns = [ 10 urlpatterns = [
10 - 11 + path('create', views.CreateSKUFlow.as_view(), name="create_sku_flow"),
  12 + path('audit/<int:pk>', views.AuditFlow.as_view(), name="audit_flow"),
  13 + path('right/<int:pk>', views.CheckAuth.as_view(), name="check_audit_auth"),
  14 + path('list', views.AuditFlowList.as_view(), name="list_flow"),
  15 + path('<int:pk>', views.FlowDetail.as_view(), name="retrieve_flow"),
  16 + path('info/<int:pk>', views.UpdateSKUInfo.as_view(), name="update_sku"),
  17 + path('template', views.ParseSKUExcel.as_view(), name="upload_sku_template"),
11 ] 18 ]
1 -from django.shortcuts import render 1 +import os
  2 +import re
  3 +from urllib import parse
2 4
3 -# Create your views here. 5 +from rest_framework.generics import CreateAPIView, RetrieveAPIView, UpdateAPIView, ListAPIView
  6 +from rest_framework.views import APIView
  7 +from rest_framework.permissions import IsAuthenticated, IsAdminUser
  8 +from rest_framework import status
  9 +import pyexcel as p
  10 +from django.db import transaction
  11 +from django.forms import model_to_dict
  12 +from django.db.models import Q
  13 +
  14 +from .models import SKU, Auditor, Flow, FlowSKU, Result, Leader
  15 +from wxProject.settings import MEDIA_ROOT
  16 +from utils.pagination import MyPageNumberPagination
  17 +from wxProject.qywx_settings import Conf, sku_conf
  18 +from utils.util import response
  19 +from utils.media import upload_media
  20 +from .serializers import FlowSerializer, SKUSerializer, FlowListSerializer, FlowSKUSerializer
  21 +from utils.helpers import WxPushHelper
  22 +
  23 +
  24 +class CreateSKUFlow(CreateAPIView):
  25 + """ 创建SKU审核流程 """
  26 + serializer_class = FlowSerializer
  27 + permission_classes = (IsAuthenticated,)
  28 +
  29 + # @transaction.atomic
  30 + def post(self, request, *args, **kwargs):
  31 + super(CreateSKUFlow, self).__init__()
  32 + skus = request.data.get('sku', None)
  33 + if not skus or skus == []:
  34 + return response(msg=u"sku不能为空", status_code=status.HTTP_400_BAD_REQUEST)
  35 + # 提交过来sku的ID
  36 + # flow 表 flow_sku表 result表
  37 + wx_client = WxPushHelper(Conf[sku_conf['APP_ID']])
  38 + with transaction.atomic():
  39 + save_id = transaction.savepoint()
  40 +
  41 + try:
  42 + skus = list(set(skus))
  43 + sku_objs = SKU.objects.filter(pk__in=skus)
  44 + flow = Flow.objects.create(starter=request.user)
  45 + flow_sku_list = [FlowSKU(sku=sku, flow=flow) for sku in sku_objs]
  46 + FlowSKU.objects.bulk_create(flow_sku_list)
  47 + auditors = Auditor.objects.all().order_by('order')
  48 + auditor_list = [Result(auditor=auditor, flow=flow) for auditor in auditors]
  49 + Result.objects.bulk_create(auditor_list)
  50 +
  51 + transaction.savepoint_commit(save_id)
  52 +
  53 + url = re.sub("PK", str(flow.id), sku_conf['flow_detail'])
  54 + url = re.sub("REDIRECT_URL", parse.quote(url, safe=''), sku_conf['wx_authorize'])
  55 + first_auditor = auditors[0]
  56 +
  57 + wx_client.push_card(first_auditor.user.wx_token, url, f"{request.user.username}发起一个毛利不达标产品审批流程")
  58 + wx_client.push_card(request.user.wx_token, url, u"流程创建成功")
  59 + # 通知创建人和审批人
  60 + return response()
  61 + except Exception as e:
  62 + print(e.args)
  63 + print(e.with_traceback)
  64 + transaction.savepoint_rollback(save_id)
  65 + return response(msg=u"创建流程有误", status_code=status.HTTP_400_BAD_REQUEST)
  66 +
  67 +
  68 +class UpdateSKUInfo(UpdateAPIView):
  69 + queryset = SKU.objects.all()
  70 + serializer_class = SKUSerializer
  71 + permission_classes = (IsAuthenticated,)
  72 +
  73 + def put(self, request, *args, **kwargs):
  74 + """ 更新 单个sku的信息 """
  75 + serializer = SKUSerializer(data=request.data)
  76 + # 查找对应用户是否拥有该sku的修改权
  77 + flow_id = request.data.get('flowID', None)
  78 + if not flow_id:
  79 + return response(msg=u"非法参数", status_code=status.HTTP_400_BAD_REQUEST)
  80 + flow_sku = FlowSKU.objects.filter(flow_id=flow_id, sku_id=kwargs['pk']).first()
  81 + if not flow_sku or flow_sku.flow.starter.id != request.user.id:
  82 + return response(msg=u"非法参数", status_code=status.HTTP_401_UNAUTHORIZED)
  83 +
  84 + if not serializer.is_valid():
  85 + raise ValueError(serializer.errors)
  86 + else:
  87 + serializer.update(instance=self.get_object(), validated_data=serializer.data)
  88 + return response()
  89 +
  90 +
  91 +class FlowDetail(RetrieveAPIView):
  92 + """ 查看当前流程 """
  93 + # 查看当前流程的sku列表 流程进度 流程结果
  94 + queryset = Flow.objects.all()
  95 + serializer_class = FlowSerializer
  96 + permission_classes = (IsAuthenticated,)
  97 +
  98 + def get(self, request, *args, **kwargs):
  99 + cur_obj = self.get_object()
  100 + cur_obj.result = []
  101 + results = Result.objects.filter(flow=cur_obj)
  102 + for i in results:
  103 + cur_obj.result.append({
  104 + "auditor": i.auditor.user.username,
  105 + "is_accept": i.is_accept,
  106 + "memo": i.memo,
  107 + })
  108 + # skus = (SKU.objects.filter(pk__in=FlowSKU.objects.filter(flow=cur_obj).values_list('sku_id', flat=True)))
  109 + # cur_obj.skus = [model_to_dict(sku) for sku in skus]
  110 + flow_skus = FlowSKU.objects.filter(flow=cur_obj)
  111 + cur_obj.skus = [FlowSKUSerializer(sku).data for sku in flow_skus]
  112 +
  113 + return response(FlowSerializer(cur_obj).data)
  114 +
  115 +
  116 +class AuditFlow(UpdateAPIView):
  117 + """ 更新当前流程状态,对流程进行审批"""
  118 + queryset = Flow.objects.all()
  119 + serializer_class = FlowSerializer
  120 + permission_classes = (IsAuthenticated, IsAdminUser)
  121 +
  122 + @staticmethod
  123 + def _check_audit(flow_obj):
  124 + # 查看是否已经全部审核完毕 进行更新flow表,只有所有sku都审批通过才能流向下一节点
  125 + # 所有的审核人员, 是否有result记录
  126 + aud_result_len = Result.objects.filter(flow=flow_obj, is_accept__isnull=False).count()
  127 + auditor_len = Auditor.objects.count()
  128 + return aud_result_len == auditor_len
  129 +
  130 + @transaction.atomic
  131 + def partial_update(self, request, *args, **kwargs):
  132 + # 对项目进行更新
  133 +
  134 + accept_choices = (
  135 + ('accept', '通过'),
  136 + ('reject', '否决')
  137 + )
  138 + # accept_param = accept_choices[0][0] if request.data.get('is_accept') else accept_choices[1][0]
  139 + skus = request.data.get('sku', [])
  140 + if len(skus) == 0 or not isinstance(request.data.get('is_accept'), bool):
  141 + return response(msg=u"非法审批参数", status_code=status.HTTP_400_BAD_REQUEST)
  142 +
  143 + result_obj = Result.objects.filter(auditor_id=self.request.user.id, is_accept__isnull=True,
  144 + flow__pk=kwargs['pk']).first()
  145 + obj = self.get_object()
  146 + if not obj or not result_obj:
  147 + raise PermissionError
  148 + else:
  149 + # 只有当前flow的所有的sku审批通过,采流下一节点审批
  150 + flow_skus = FlowSKU.objects.filter(flow=obj)
  151 + accept_bool = bool(request.data.get('is_accept'))
  152 + if len(flow_skus) == len(request.data.get('sku')):
  153 + accept_param = accept_choices[0][0]
  154 + else:
  155 + obj.is_done = True
  156 + accept_param = accept_choices[1][0]
  157 + result_obj.is_accept = accept_param
  158 + result_obj.memo = request.data.get('memo', '')
  159 + result_obj.save()
  160 + for flow_sku in flow_skus:
  161 + if flow_sku.sku.id in skus:
  162 + flow_sku.is_pass = accept_bool
  163 + else:
  164 + flow_sku.is_pass = not accept_bool
  165 + flow_sku.save()
  166 +
  167 + wx_client = WxPushHelper(Conf[sku_conf['APP_ID']])
  168 + full_audit_done = self._check_audit(obj)
  169 + url = re.sub("PK", str(obj.id), sku_conf['flow_detail'])
  170 + url = re.sub("REDIRECT_URL", parse.quote(url, safe=''), sku_conf['wx_authorize'])
  171 + desc = "返单特采流程所有审批已完成" if full_audit_done else f"{request.user.username}已审批完成"
  172 + if full_audit_done:
  173 + obj.is_done = True
  174 + else:
  175 + if not bool(request.data.get('is_accept')):
  176 + obj.is_done = True
  177 + else:
  178 + next_auditor_id = Result.objects.filter(flow=obj, is_accept__isnull=True) \
  179 + .values_list('auditor_id', flat=True).order_by('pk').first()
  180 + second_auditor = Auditor.objects.filter(pk=next_auditor_id).first()
  181 + wx_client.push_card(second_auditor.user.wx_token, url, f"{request.user.username}已审核了一个返单特采申请")
  182 + obj.save()
  183 + wx_client.push_card(obj.starter.wx_token, url, desc)
  184 +
  185 + return response(FlowSerializer(obj).data)
  186 +
  187 +
  188 +class AuditFlowList(ListAPIView):
  189 + """ 查看当前审核人员名下的审核清单 """
  190 + queryset = Result.objects.all()
  191 + serializer_class = FlowListSerializer
  192 + pagination_class = MyPageNumberPagination
  193 + permission_classes = (IsAuthenticated,)
  194 +
  195 + def get_queryset(self):
  196 + data = Result.objects.filter(auditor_id=self.request.user.id).order_by('is_accept', '-create_time')
  197 + for item in data:
  198 + # result = Flow.objects.filter(=self.request.user).values_list('is_done', flat=True).first()
  199 + item.creator_name = item.flow.starter.username
  200 + item.result = True if item.is_accept else False
  201 + item.flow_id = item.flow.id
  202 + return data
  203 +
  204 +
  205 +class CheckAuth(APIView):
  206 + """ 查看当前查看用户是否有审批权限"""
  207 + allowed_methods = ('GET',)
  208 + permission_classes = (IsAuthenticated,)
  209 +
  210 + @staticmethod
  211 + def get(request, *args, **kwargs):
  212 + flow_result = Result.objects.filter(flow__pk=kwargs['pk'])
  213 + # auditor=request.user) | Q(flow__starter=request.user)
  214 + if not flow_result \
  215 + or flow_result.result_auditor.user.id != request.user.id:
  216 + return response(False)
  217 + else:
  218 + order = int(Auditor.objects.filter(user=request.user).values_list('order', flat=True).first())
  219 + if order > 0:
  220 + # 当审核人员排队时 需要判断是否已经流转到自己 前一个人员是否已经有处理
  221 + # 还要判断自己是否已经审核过了
  222 + index = order - 1
  223 + if bool(flow_result[index].is_accept) and not bool(flow_result[order].is_accept):
  224 + return response(True)
  225 + else:
  226 + return response(False)
  227 + else:
  228 + return response(False) if bool(flow_result[0].is_accept) else response(True)
  229 +
  230 +
  231 +class ParseSKUExcel(CreateAPIView):
  232 + """ 解析上传excel的SKU数据"""
  233 + serializer_class = SKUSerializer
  234 + permission_classes = (IsAuthenticated,)
  235 + allowed_extension = (".xls", ".xlsx")
  236 +
  237 + def post(self, request, *args, **kwargs):
  238 + super(ParseSKUExcel, self).__init__()
  239 + file_obj = self._upload(request)
  240 + # 解析sku列表 插入数据库
  241 + path = os.path.join(MEDIA_ROOT, str(file_obj.file))
  242 + t = p.get_sheet(file_name=path, start_row=1, column_limit=16)
  243 + with transaction.atomic():
  244 + save_id = transaction.savepoint()
  245 + try:
  246 + s_list = self._insert_data(list(t.rows()))
  247 + transaction.savepoint_commit(save_id)
  248 + return response(s_list)
  249 + except ValueError as e:
  250 + transaction.savepoint_rollback(save_id)
  251 + return response(msg=u"文件上传数据有误", status_code=status.HTTP_400_BAD_REQUEST)
  252 + except IndexError as e:
  253 + transaction.savepoint_rollback(save_id)
  254 + return response(msg=u"文件上传数据有误", status_code=status.HTTP_400_BAD_REQUEST)
  255 + except Exception as e:
  256 + transaction.savepoint_rollback(save_id)
  257 + return response(msg=u"文件上传数据有误", status_code=status.HTTP_400_BAD_REQUEST)
  258 +
  259 + @staticmethod
  260 + def _insert_data(rows):
  261 + obj_list = []
  262 + for row in rows:
  263 + if len(row) < 16:
  264 + raise IndexError("上传数据字段不完整")
  265 + qty = 0 if row[5] == "" or row[5] == "#N/A" else int(row[5])
  266 + price_with_tax = 0 if row[6] == "" or row[6] == "#N/A" else float(row[6])
  267 + amount_with_tax = 0 if row[7] == "" or row[7] == "#N/A" else float(row[7])
  268 + sell_day = 0 if row[8] == "" or row[8] == "#N/A" else int(row[8])
  269 + qty_within_30 = 0 if row[9] == "" or row[9] == "#N/A" else int(row[9])
  270 + inventory = 0 if row[10] == "" or row[10] == "#N/A" else int(row[10])
  271 + coming_inventory = 0 if row[11] == "" or row[11] == "#N/A" else int(row[11])
  272 + gross_profit_rate = 0 if row[12] == "" or row[12] == "#N/A" else float(row[12])
  273 + return_rate = 0 if row[13] == "" or row[13] == "#N/A" else float(row[13])
  274 + memo = "" if row[14] == "#N/A" else row[14].strip()
  275 + purchaser = row[15].strip()
  276 +
  277 + obj = SKU.objects.create(
  278 + supplier=row[0],
  279 + sku=row[1],
  280 + model=row[2],
  281 + title=row[3],
  282 + is_new=row[4],
  283 + qty=qty,
  284 + price_with_tax=price_with_tax,
  285 + amount_with_tax=amount_with_tax,
  286 + sell_day=sell_day,
  287 + qty_within_30=qty_within_30,
  288 + inventory=inventory,
  289 + coming_inventory=coming_inventory,
  290 + gross_profit_rate=gross_profit_rate,
  291 + return_rate=return_rate,
  292 + memo=memo,
  293 + purchaser=purchaser,
  294 +
  295 + )
  296 + obj_list.append(SKUSerializer(obj).data)
  297 + return obj_list
  298 +
  299 + def _upload(self, request):
  300 + excel = request.FILES.get('file')
  301 +
  302 + if not excel:
  303 + return response(msg=u"文件上传失败", status_code=status.HTTP_400_BAD_REQUEST)
  304 + excel.name = excel.name.strip('"')
  305 + ext_pos = excel.name.rfind('.')
  306 + uploaded_file_ext = excel.name[ext_pos:]
  307 + if uploaded_file_ext not in self.allowed_extension:
  308 + return response(msg="非法文件类型", status_code=status.HTTP_400_BAD_REQUEST)
  309 + return upload_media(excel, uploaded_file_ext, request.user)
@@ -59,7 +59,7 @@ class WxPushHelper: @@ -59,7 +59,7 @@ class WxPushHelper:
59 "msgtype": "textcard", 59 "msgtype": "textcard",
60 "agentid": self.conf['APP_ID'], # 企业应用ID 60 "agentid": self.conf['APP_ID'], # 企业应用ID
61 "textcard": { 61 "textcard": {
62 - "title": "产品立项流程通知", 62 + "title": f"{self.conf['title']}流程通知",
63 "description": description, 63 "description": description,
64 "url": url, 64 "url": url,
65 "btntxt": "点击查看" 65 "btntxt": "点击查看"
@@ -15,21 +15,8 @@ from rest_framework.permissions import IsAuthenticated, AllowAny @@ -15,21 +15,8 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
15 from rest_framework import status 15 from rest_framework import status
16 16
17 17
18 -class UploadMedia(CreateAPIView):  
19 - serializer_class = MediaSerializer  
20 - permission_classes = (IsAuthenticated, )  
21 -  
22 - def post(self, request, *args, **kwargs):  
23 - super(UploadMedia, self).__init__()  
24 - uploaded_file = request.FILES.get('file')  
25 - uploaded_file.name = uploaded_file.name.strip('"')  
26 -  
27 - if not uploaded_file:  
28 - return response(msg=u"文件上传失败", status_code=status.HTTP_400_BAD_REQUEST)  
29 - ext_pos = uploaded_file.name.rfind('.')  
30 - uploaded_file_ext = uploaded_file.name[ext_pos:]  
31 - if not self.allowed_extension(uploaded_file_ext):  
32 - return response(msg="非法文件类型", status_code=status.HTTP_400_BAD_REQUEST) 18 +def upload_media(uploaded_file, uploaded_file_ext, user):
  19 + """ 处理上传新建媒体记录 """
33 hash_file_name = uuid() + uploaded_file_ext 20 hash_file_name = uuid() + uploaded_file_ext
34 time_tag = time.strftime('%Y-%m-%d') 21 time_tag = time.strftime('%Y-%m-%d')
35 file_path = time_tag + os.sep + hash_file_name 22 file_path = time_tag + os.sep + hash_file_name
@@ -48,17 +35,35 @@ class UploadMedia(CreateAPIView): @@ -48,17 +35,35 @@ class UploadMedia(CreateAPIView):
48 file=uploaded_file, 35 file=uploaded_file,
49 file_name=uploaded_file.name, 36 file_name=uploaded_file.name,
50 hash=file_hash, 37 hash=file_hash,
51 - user=request.user, 38 + user=user,
52 size=file_size, 39 size=file_size,
53 extension=uploaded_file_ext 40 extension=uploaded_file_ext
54 ) 41 )
55 os.remove(os.path.join(MEDIA_ROOT, file_path)) 42 os.remove(os.path.join(MEDIA_ROOT, file_path))
56 - # return response.Response(MediaSerializer(file, context={'request': request}).data) 43 + return file
  44 +
  45 +
  46 +class UploadMedia(CreateAPIView):
  47 + serializer_class = MediaSerializer
  48 + permission_classes = (IsAuthenticated, )
  49 +
  50 + def post(self, request, *args, **kwargs):
  51 + super(UploadMedia, self).__init__()
  52 + uploaded_file = request.FILES.get('file')
  53 + uploaded_file.name = uploaded_file.name.strip('"')
  54 +
  55 + if not uploaded_file:
  56 + return response(msg=u"文件上传失败", status_code=status.HTTP_400_BAD_REQUEST)
  57 + ext_pos = uploaded_file.name.rfind('.')
  58 + uploaded_file_ext = uploaded_file.name[ext_pos:]
  59 + if not self.allowed_extension(uploaded_file_ext):
  60 + return response(msg="非法文件类型", status_code=status.HTTP_400_BAD_REQUEST)
  61 + file = upload_media(uploaded_file, uploaded_file_ext, request.user)
57 return response(MediaSerializer(file, context={'request': request}).data) 62 return response(MediaSerializer(file, context={'request': request}).data)
58 63
59 @staticmethod 64 @staticmethod
60 - def allowed_extension(ext):  
61 - choices = UPLOAD_MEDIA_CHOICES 65 + def allowed_extension(ext, allowed_ext=None):
  66 + choices = allowed_ext if allowed_ext else UPLOAD_MEDIA_CHOICES
62 for allowed_ext in choices: 67 for allowed_ext in choices:
63 if ext == allowed_ext[0]: 68 if ext == allowed_ext[0]:
64 return True 69 return True
@@ -11,13 +11,17 @@ project_conf = { @@ -11,13 +11,17 @@ project_conf = {
11 "CORP_ID": "ww0f3efc2873ad11c3", 11 "CORP_ID": "ww0f3efc2873ad11c3",
12 "APP_ID": '1000078', 12 "APP_ID": '1000078',
13 "APP_SECRET": "7MHpdQICiegx9rIc4iZrEPunb1aYUqdJYKSW9v7a1A8", 13 "APP_SECRET": "7MHpdQICiegx9rIc4iZrEPunb1aYUqdJYKSW9v7a1A8",
  14 + "wx_authorize": "https://open.weixin.qq.com/connect/oauth2/authorize?appid=ww0f3efc2873ad11c3&redirect_uri=REDIRECT_URL&response_type=code&scope=snsapi_base&state=1000078#wechat_redirect",
  15 + "flow_detail": "http://project.tacklifetools.com/product/audit/PK"
14 } 16 }
15 17
16 sku_conf = { 18 sku_conf = {
17 - "title": "sku条目审批应用", 19 + "title": "返单特采",
18 "CORP_ID": "ww0f3efc2873ad11c3", 20 "CORP_ID": "ww0f3efc2873ad11c3",
19 "APP_ID": '1000081', 21 "APP_ID": '1000081',
20 "APP_SECRET": "_O_MrxbQO1vojzNBPAiaF_MdzikHRbnVfFc3v8iOtKo", 22 "APP_SECRET": "_O_MrxbQO1vojzNBPAiaF_MdzikHRbnVfFc3v8iOtKo",
  23 + "wx_authorize": "https://open.weixin.qq.com/connect/oauth2/authorize?appid=ww0f3efc2873ad11c3&redirect_uri=REDIRECT_URL&response_type=code&scope=snsapi_base&state=1000081#wechat_redirect",
  24 + "flow_detail": "http://project.tacklifetools.com/sku/audit/PK"
21 } 25 }
22 26
23 27
@@ -25,3 +29,4 @@ Conf = { @@ -25,3 +29,4 @@ Conf = {
25 "1000078": project_conf, 29 "1000078": project_conf,
26 "1000081": sku_conf 30 "1000081": sku_conf
27 } 31 }
  32 +
@@ -177,7 +177,3 @@ SIMPLE_JWT = { @@ -177,7 +177,3 @@ SIMPLE_JWT = {
177 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), 177 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
178 } 178 }
179 179
180 -FRONT_URL = {  
181 - "wx_authorize": "https://open.weixin.qq.com/connect/oauth2/authorize?appid=ww0f3efc2873ad11c3&redirect_uri=REDIRECT_URL&response_type=code&scope=snsapi_base#wechat_redirect",  
182 - "flow_detail": "http://project.tacklifetools.com/product/audit/PK"  
183 -}  
@@ -18,6 +18,7 @@ urlpatterns = [ @@ -18,6 +18,7 @@ urlpatterns = [
18 path(api_version + 'auth/', include('usercenter.urls', namespace='usercenter')), 18 path(api_version + 'auth/', include('usercenter.urls', namespace='usercenter')),
19 path(api_version + 'upload/', UploadMedia.as_view()), 19 path(api_version + 'upload/', UploadMedia.as_view()),
20 path(api_version + 'project/', include('project.urls', namespace="project")), 20 path(api_version + 'project/', include('project.urls', namespace="project")),
  21 + path(api_version + 'sku/', include('sku.urls', namespace="sku")),
21 path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), 22 path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
22 re_path(r'api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 23 re_path(r'api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
23 re_path(r'api/token/', MyTokenObtainPairView.as_view(), name='token_obtain_pair'), 24 re_path(r'api/token/', MyTokenObtainPairView.as_view(), name='token_obtain_pair'),