完成客户报名的流程
流程大体如下:在已有收集的客户信息基础上——>销售填写报名表(报什么班、课程顾问)——>自动生成一个链接,让学员填写——>学员填写个人信息,并上传身份照片,同意合同协议——>销售审核信息和合同——>学员缴费,生成缴费——>报名完成,状态改为已报名。
销售点击相关客户的报名链接:
销售录入报名信息:
生成链接给学员填报:
学员按照给定的链接,填报信息:
销售审核信息:
审核通过,生成缴费信息:
缴费成功,修改状态:
代码实现:
一、销售填写报名信息并生成报名信息链接
添加路由项:path('customer/<int:id_num>/enrollment/',views.enrollment,name='enrollment'),当点击报名链接时,此链接地址:<a href='/plcrm/customer/%s/enrollment/'>%s</a>,就跳转到views.enrollment函数:
@login_required
def enrollment(req,id_num):
customer_obj = models.Customer.objects.get(id=id_num)
# 因为生成的modelform中没有customer,这里通过id查询到customer,然后返回给前端
msgs = {}
if req.method == "POST":
enroll_form = forms.EnrollmentForm(req.POST)
if enroll_form.is_valid():
msg = '''请将下面链接发送给客户进行填写:
http://127.0.0.1:8000/plcrm/customer/registration/{enroll_obj_id}/{random_str}/'''
try:
print("cleandata:",enroll_form.cleaned_data)
enroll_form.cleaned_data["customer"] = customer_obj
# 通过前面查询到的customer,这里手工添加到form的cleaned_data中,主要是前端表单中没有传递这一项
enroll_obj = models.Enrollment.objects.create(**enroll_form.cleaned_data)
random_str = "".join(random.sample(string.ascii_lowercase + string.digits, 8))
cache.set(enroll_obj.id,random_str,3000)
msgs['msg'] = msg.format(enroll_obj_id=enroll_obj.id,random_str=random_str )
except IntegrityError as e:
# 前面通过手动添加customer到modelform的cleaned_data中,没有经过验证,所以同样的记录会被保存到数据库,会引发联合唯一错误
# 这里通过捕获错误,给modelform的errors增加错误项,进行信息提示和流程阻止
enroll_obj = models.Enrollment.objects.get(customer_id=customer_obj.id,
enrolled_class_id=enroll_form.cleaned_data['enrolled_class'].id)
if enroll_obj.contract_agreed: # 学生已经同意
return redirect('/plcrm/contract_review/%s/' %enroll_obj.id)
enroll_form.add_error("__all__", "该用户此条报名信息已存在,不能重复")
random_str = "".join(random.sample(string.ascii_lowercase+string.digits,8))
cache.set(enroll_obj.id, random_str, 3000)
msgs['msg'] = msg.format(enroll_obj_id=enroll_obj.id,random_str=random_str)
else:
enroll_form = forms.EnrollmentForm()
return render(req,"sales/enrollment.html",{'enroll_form':enroll_form,"customer_obj":customer_obj,"msgs":msgs})
点击报名时,是GET方式进入函数views.enrollment,此时执行else语句体,就是生成一个空的eroll_form,然后返回给前端页面enrollment.html。在这个页面填写信息后点击下一步,提交信息,再次进入此函数,是以POST方式进入,执行POST方法语句体,判断form是否有错误,即先是form级验证,验证通过,保存数据库,即model的create方法,因为有可能保存数据库失败,即唯一性错误,这里进行异常捕获,出错了,要进行提示,销售进行修改。一切都正常后,要给出一条学员信息填写的链接,这里这个链接为了防止被爆破,增加了一个随机串,并将其保存到cache中,设置有效时间,这样定时更改学员信息填报链接。
用到的EnrollmentForm:
class EnrollmentForm(ModelForm):
def __new__(cls, *args, **kwargs):
for field_name,field_obj in cls.base_fields.items():
field_obj.widget.attrs['class'] = 'form-control'
return ModelForm.__new__(cls)
class Meta:
model = models.Enrollment
fields = ['enrolled_class','consultant']
前端页面enrollment.html部分代码
<div class="container">
<div class="row"><hr/></div>
<div class="row">学生报名信息录入</div>
<div class="row"><hr/></div>
<form class="form-group" role="form"method="post">{% csrf_token %}
<span style="color: red;">{{ enroll_form.errors }}</span>
<div class="row">
<div class="col-2"><h4>客户:</h4></div>
<div class="col-4"><h4>qq:{{ customer_obj.qq }} name:{{ customer_obj.qq_name }}<h4/></div>
</div>
{% for field in enroll_form %}
<div class="row">
<div class="col-2"><label>{{ field.label }}</label></div>
<div class="col-4">{{ field }}</div>
</div>
{% endfor %}
<div class="row">
<input type="submit" class="btn btn-info pull-right" value="下一步">
</div>
</form>
<div class="row">
<div>
{% for k,v in msgs.items %}
<li>{{ k }}:<br>{{ v }}</li>
{% endfor %}
</div>
</div>
</div>
二、学员报名信息填写实现
按照给学员的链接,在路由表中添加路由项:path('customer/registration/<int:id_num>/<str:random_str>/',views.stu_registration,name='stu_registration'),
编写学员报名信息填报处理函数views.stu_registration:
def stu_registration(req,id_num,random_str):
# 学生报名相关信息,主要是一个form
if cache.get(id_num) == random_str:
enroll_obj = models.Enrollment.objects.get(id=id_num)
if req.method == "POST":
if req.is_ajax(): # 这里处理dropzone使用ajax上传图片的过程,将图片保存
print("ajax post:",req.FILES)
enroll_data_dir = "%s/%s" %(settings.ENROLL_DATA,id_num)
if not os.path.exists(enroll_data_dir):
os.makedirs(enroll_data_dir,exist_ok=True)
for k,file_obj in req.FILES.items():
with open("%s/%s" %(enroll_data_dir,file_obj.name),"wb") as f:
for chunk in file_obj.chunks():
f.write(chunk)
return HttpResponse("success")
customer_form = forms.CustomerForm(req.POST,instance=enroll_obj.customer)
if customer_form.is_valid():
customer_form.save() # 用户Form表单验证通过,保存学员填报的信息
enroll_obj.contract_agreed = True
enroll_obj.save() # 数据库报名表中将合同协议同意字段改为True,即学员同意合同
return render(req,"sales/stu_registration.html",{"enrolled_obj":enroll_obj,"status":1}) # 保存成功返回一个报名成功提示页面
else: # get方法进入时,要判断学员是否同意合同项,如果该项为True,说明已经提交过,设置状态字status为1,否则为0
if enroll_obj.contract_agreed == True:
status = 1
else:
status = 0
customer_form = forms.CustomerForm(instance=enroll_obj.customer)
return render(req,"sales/stu_registration.html",{"customer_form":customer_form,"enrolled_obj":enroll_obj,'status':status})
else:
return HttpResponse("不要乱试了")
用到的CustomerForm:
class CustomerForm(ModelForm):
def __new__(cls, *args, **kwargs):
for field_name,field_obj in cls.base_fields.items():
field_obj.widget.attrs['class'] = 'form-control'
if field_name in cls.Meta.readonly_fields:
field_obj.widget.attrs['disabled'] = 'disabled'
return ModelForm.__new__(cls)
def clean_qq(self):
print('--------1',self.cleaned_data['qq']) # 这一步打印是有数据的
if self.instance.qq != self.cleaned_data['qq']:
self.add_error("qq","不允许修改QQ号")
print('--------2', self.cleaned_data['qq']) # 这一步打印,就出错,KeyError:‘qq’,说明cleaned_data中清除了qq字段
# return self.cleaned_data['qq'] # 一开始返回的是这个,感觉都一样,但是此时cleaned_data中没有qq字段了,会出错
return self.instance.qq
def clean_consultant(self):
if self.instance.consultant != self.cleaned_data['consultant']:
self.add_error("consultant","不允许修改consultant号")
return self.instance.consultant
def clean_source(self):
if self.instance.source != self.cleaned_data['source']:
self.add_error("source","不允许修改source号")
return self.instance.source
class Meta:
model = models.Customer
fields = "__all__"
exclude = ['tags','content','memo','status','referral_form','consult_course']
readonly_fields = ['qq','consultant','source',]
前端页面stu_registration.html
{% extends "base.html" %}
{% load plcrm_tags %}
{% block mybody %}
<body>
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">我的客户管理系统</a>
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse" data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search" -->
<ul class="navbar-nav px-3">
<li class="nav-item text-nowrap">
<a class="nav-link" href="#">{{ request.user.name }}</a>
</li>
</ul>
</nav>
<div class="container pt-3">
<div class="card">
<div class="card-header bg-info text-light">
Xxx教育学院 | 报名入学
</div>
<div class="card-body ml-3">
{{ customer_form.errors }}
{% if status != 1 %}
<blockquote class="blockquote mb-0">
客户信息:
</blockquote>
<form method="post" onsubmit="return RegisterFormCheck();">{% csrf_token %}
{% for field in customer_form %}
<div class="form-group row m-sm-0">
<label for="staticEmail" class="col-sm-2 col-form-label p-0">{{ field.label }}</label>
<div class="col-sm-6 p-0">
{{ field }}
</div>
</div>
{% endfor %}
<hr/>
<blockquote class="blockquote mb-0">
所报班级信息:
</blockquote>
<div class="form-group row m-sm-0">
<label for="staticEmail" class="col-sm-2 col-form-label">班级信息</label>
<div class="col-sm-6">
{{ enrolled_obj.enrolled_class }}
</div>
</div>
<div class="form-group row m-sm-0">
<label for="staticEmail" class="col-sm-2 col-form-label">课程费用</label>
<div class="col-sm-6">
{{ enrolled_obj.enrolled_class.course.price }}
</div>
</div>
<div class="form-group row m-sm-0">
<label for="staticEmail" class="col-sm-2 col-form-label">开课日期</label>
<div class="col-sm-6">
{{ enrolled_obj.enrolled_class.start_date }}
</div>
</div>
<hr/>
<blockquote class="blockquote mb-0">
合同信息:
</blockquote>
<div class="form-group row m-sm-0">
<div class="col-sm-12">
<pre style="height: 200px;overflow: auto;">{% render_enroll_contract enrolled_obj %}</pre>
</div>
</div>
<div class="form-group row m-sm-0">
<div class="col-sm-12">
<input type="checkbox" name="contract_agreed"> 已认真阅读完协议并接受所有条款
</div>
</div>
<div class="text-center">
<input class="btn btn-info text-center" type="submit" value="提交">
</div>
</form>
<hr/>
{# <div id="mydropz" class="dropzone svelte-12uhhij dz-clickable">#}
{# </div>#}
<form id="mydropz" action={{ request.path }} class="dropzone">
<div class="fallback">
<input name="file" type="file" multiple />
</div>
</form>
{% else %}
<blockquote class="blockquote mb-0">
{{ enrolled_obj.customer }},你已提交成功
</blockquote>
{% endif %}
</div>
</div>
</div>
<script>
{#$("form#mydropz").dropzone({dictDefaultMessage:'拖拽上传文件到此',method:"post",#}
{# headers: {'X-CSRFToken':'{{csrf_token}}'}})#}
{# 注意这里的headers设置,是将csrf_token带上,否则将出现Forbidden (CSRF token missing or incorrect.)错误 #}
{#使用上面的写法会出现Uncaught Error: Dropzone already attached.错误,说明$("form#mydropz").dropzone是再次初始化,使用下面的写法 #}
{#Dropzone.options.mydropz = {#}
{# headers:{'X-CSRFToken':'{{csrf_token}}'},#}
{# dictDefaultMessage:"kkkkkkkkkkkkkk",#}
{# init:function () {#}
{# this.on("success",function (file,data) {#}
{# console.log(file);#}
{# console.log(data);#}
{##}
{# })#}
{# }#}
{#}#}
if ($("form#mydropz").length !=0) {
Dropzone.options.mydropz = false;
var myDropzone = new Dropzone("form#mydropz",{
headers:{'X-CSRFToken':'{{csrf_token}}'},
dictDefaultMessage:"图片拖拽到此",
});
}
{#$(function () {#}
{# Dropzone.options.mydropz = {#}
{# maxFiles:2,#}
{# addRemoveLinks:true,#}
{# uploadMultiple:true,#}
{# headers: {'X-CSRFToken':'{{csrf_token}}'},#}
{# accept:function (file,done) {#}
{# if (file.name == "justinbieber.jpg") {#}
{# done("Naha,don't");#}
{# }#}
{# else {done();}#}
{# }#}
{# }#}
{# });#}
function RegisterFormCheck() {
if (myDropzone.files.length<2){
alert("至少上传两张图片");
return false;
}
if ($("form input:checkbox").prop('checked')){
$('form').find("[disabled]").removeAttr("disabled")
return true;
}else {
alert("必须同意条款");
return false;
}
}
</script>
</body>
<script>
</script>
{% endblock %}
这个过程中,主要在于dropzone这个组件的使用。
三、销售人员审核:
接第一步,当学员信息提报以后,销售再次点击下一步按钮时,因为contract_agreed为True,进入合同审核过程,即执行:
if enroll_obj.contract_agreed: # 学生已经同意
return redirect('/plcrm/contract_review/%s/' %enroll_obj.id)
在路由表中增加路由项:path('contract_review/<int:enroll_id>/',views.contract_review,name='contract_review'),
编写函数views.contract_review:
def contract_review(req,enroll_id):
enroll_obj = models.Enrollment.objects.get(id=enroll_id) # 获取报名信息对象
enroll_form = forms.EnrollmentForm(instance=enroll_obj) # 报名信息的Form,由于展示
customer_form = forms.CustomerForm(instance=enroll_obj.customer) # 学员信息Form
return render(req,'sales/contract_review.html',{'enroll_obj':enroll_obj,
'enroll_form':enroll_form,
'customer_form':customer_form})
前端页面contract_review.html:
<div class="container pt-3">
<div class="card">
<div class="card-header bg-info text-light">
<h3>学员信息审核</h3>
</div>
<hr/>
<div class="card-body ml-3">
<blockquote class="blockquote mb-0">
学员信息:
</blockquote>
<form method="post" >{% csrf_token %}
{% for field in customer_form %}
<div class="form-group row m-sm-0">
<label for="staticEmail" class="col-sm-2 col-form-label p-0">{{ field.label }}</label>
<div class="col-sm-6 p-0">
{{ field }}
</div>
</div>
{% endfor %}
<hr/>
<blockquote class="blockquote mb-0">
报名信息:
</blockquote>
{% for field in enroll_form %}
<div class="form-group row m-sm-0">
<label for="staticEmail" class="col-sm-2 col-form-label p-0">{{ field.label }}</label>
<div class="col-sm-6 p-0">
{{ field }}
</div>
</div>
{% endfor %}
<hr/>
<div class="clearfix col-sm-8">
<a href="{% url 'enrollment_rejection' enroll_obj.id %}" class="btn btn-danger float-left" >审核驳回</a>
<a href="{% url 'payment' enroll_obj.id %}" class="btn btn-info float-right" >审核通过</a>
</div>
</form>
</div>
</div>
</div>
功能就是显示学员基本信息和报名信息,这里提供审核驳回和审核通过两项,审核驳回,就是将报名信息的contract_agreed置为False,就是学员同意协议置为False,这样,学员又可以重新填写相关信息。
审核通过,跳转到缴费页面
四、学员缴费
审核通过后跳转到payment路由项,增加此项
path('payment/<int:enroll_id>/',views.payment,name='payment'),
函数views.payment:
def payment(req,enroll_id):
enroll_obj = models.Enrollment.objects.get(id=enroll_id)
errors = []
if req.method == "POST":
payment_amount = req.POST.get('amount')
if payment_amount and int(payment_amount)>500:
payment_obj = models.Payment.objects.create(
customer=enroll_obj.customer,
course= enroll_obj.enrolled_class.course,
amount=int(payment_amount),
consultant=enroll_obj.consultant,
)
enroll_obj.contract_approved = True
enroll_obj.save()
enroll_obj.customer.status = 0
enroll_obj.customer.save()
return redirect("/mytestapp/plcrm/customer/")
else:
errors.append("缴费金额不能低于500")
return render(req,'sales/payment.html',{'enroll_obj':enroll_obj,
'errors':errors})
主要功能是对缴费金额进行确认保存,费用不能低于500,保存缴费金额后,需要同时修改报名表中的contract_approved为True,表明报名生效,同时修改客户表中的状态status为0,表明客户已报名。
前端页面payment.html:
<div class="container pt-3">
<div class="card">
<div class="card-header bg-info text-light">
<h3>学员信缴费</h3>
</div>
<hr/>
<div class="card-body ml-3">
<blockquote class="blockquote mb-0">
学员报名基本信息:
</blockquote>
<ul style="color: red;">
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
<form method="post" action="">{% csrf_token %}
<div class="form-group row m-sm-0">
<label for="staticEmail" class="col-sm-2 col-form-label p-0">客户:</label>
<div class="col-sm-6 p-0">{{ enroll_obj.customer }}</div>
</div>
<div class="form-group row m-sm-0">
<label for="staticEmail" class="col-sm-2 col-form-label p-0">所报课程:</label>
<div class="col-sm-6 p-0">{{ enroll_obj.enrolled_class }}</div>
</div>
<div class="form-group row m-sm-0">
<label for="staticEmail" class="col-sm-2 col-form-label p-0">课程缴费:</label>
<div class="col-sm-6 p-0">
<input type="text" name="amount" placeholder="缴费金额不能小于500">
</div>
</div>
<div class="form-group row m-sm-0">
<label for="staticEmail" class="col-sm-2 col-form-label p-0">课程顾问:</label>
<div class="col-sm-6 p-0">{{ enroll_obj.consultant }}</div>
</div>
<hr/>
<div class="clearfix col-sm-8 offset-sm-3">
<input class="btn btn-info" type="submit" value="缴费提交,开启学习">
</div>
</form>
</div>
</div>
</div>
至此,整个报名过程结束。