第一次提交

master
laoxiao 1 year ago
commit 698420c493

8
.idea/.gitignore vendored

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/../../../../:\py_workspace\ERP_5\.idea/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="ERP_5/settings.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/ERP_5/apps" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/../ERP_5\templates" />
</list>
</option>
</component>
</module>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="erp_5" uuid="4bc53945-f65a-487e-8aae-a32e60fe31b7">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/erp_5</jdbc-url>
</data-source>
</component>
</project>

@ -0,0 +1,2 @@
#n:information_schema
!<md> [null, 0, null, null, -2147483648, -2147483648]

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
<serverData>
<paths name="root@192.168.23.3:22 agent">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="root@192.168.23.3:22 password">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

@ -0,0 +1,33 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="6">
<item index="0" class="java.lang.String" itemvalue="mysqlclient" />
<item index="1" class="java.lang.String" itemvalue="install" />
<item index="2" class="java.lang.String" itemvalue="Django" />
<item index="3" class="java.lang.String" itemvalue="urllib3" />
<item index="4" class="java.lang.String" itemvalue="redis" />
<item index="5" class="java.lang.String" itemvalue="idna" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="E501" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N802" />
</list>
</option>
</inspection_tool>
</profile>
</component>

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (django-3.2.16_env) (3)" project-jdk-type="Python SDK" />
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ERP_5.iml" filepath="$PROJECT_DIR$/.idea/ERP_5.iml" />
</modules>
</component>
</project>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/datas/nation_data.sql" dialect="GenericSQL" />
<file url="PROJECT" dialect="MySQL" />
</component>
</project>

@ -0,0 +1,4 @@
from .celery import celery_app
__all__ = ('celery_app',)

@ -0,0 +1 @@
# 存放所有项目中APP

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,9 @@
from django.apps import AppConfig
class BasicInfoConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'basic_info'
def ready(self):
import basic_info.signals

@ -0,0 +1,153 @@
from django.db import models
# 区域的模型类
from ERP_5.utils.base_model import BaseModel
class Nation(models.Model):
id = models.IntegerField(primary_key=True)
n_name = models.CharField('国家名称', max_length=30)
class Meta:
db_table = 'nation'
verbose_name = '国家表'
verbose_name_plural = verbose_name
ordering = ['id']
def __str__(self):
return self.n_name
# 省份的模型类
class Province(models.Model):
id = models.IntegerField(primary_key=True)
p_name = models.CharField('省份名称', max_length=30)
nation = models.ForeignKey('Nation', related_name='province_list', on_delete=models.CASCADE, verbose_name='省份所在的国家')
class Meta:
db_table = 'province'
verbose_name = '省份表'
verbose_name_plural = verbose_name
ordering = ['id']
def __str__(self):
return self.p_name
# 城市的模型类
class City(models.Model):
id = models.IntegerField(primary_key=True)
c_name = models.CharField('城市名称', max_length=30)
province = models.ForeignKey('Province', related_name='city_list', on_delete=models.CASCADE, verbose_name='城市所在的省份')
class Meta:
db_table = 'city'
verbose_name = '城市表'
verbose_name_plural = verbose_name
ordering = ['id']
def __str__(self):
return self.c_name
# 供应商模型类
class SupplierModel(BaseModel):
name = models.CharField(max_length=100, verbose_name='供应商名称', unique=True)
mobile = models.CharField('手机号码', max_length=11, blank=True, null=True)
phone = models.CharField('联系电话', max_length=22, blank=True, null=True)
contacts_name = models.CharField('联系人名', max_length=22, blank=True, null=True)
email = models.CharField('电子邮箱', max_length=50, blank=True, null=True)
ratepayer_number = models.CharField('纳税人识别号码', max_length=50, blank=True, null=True)
bank = models.CharField('开户银行', max_length=50, blank=True, null=True)
account_number = models.CharField('银行账号', max_length=50, blank=True, null=True)
nation = models.CharField('国家', max_length=50, blank=True, null=True)
province = models.CharField('省份', max_length=50, blank=True, null=True)
city = models.CharField('城市', max_length=50, blank=True, null=True)
address = models.CharField('详细地址', max_length=50, blank=True, null=True)
remark = models.CharField('备注', max_length=512, blank=True, null=True)
init_pay = models.DecimalField('初期应付', max_digits=20, decimal_places=2, blank=True, default=0) # 精确到小数点后两位
current_pay = models.DecimalField('末期应付', max_digits=20, decimal_places=2, default=0, blank=True) # 精确到小数点后两位
order_number = models.IntegerField('排序号码', default=100)
class Meta:
db_table = 't_supplier'
verbose_name = '供应商'
verbose_name_plural = verbose_name
ordering = ['order_number', 'id']
def __str__(self):
return self.name
# 客户模型类
class CustomerModel(BaseModel):
name = models.CharField(max_length=100, verbose_name='客户名称', unique=True)
mobile = models.CharField('手机号码', max_length=11, blank=True, null=True)
phone = models.CharField('联系电话', max_length=22, blank=True, null=True)
contacts_name = models.CharField('联系人名', max_length=22, blank=True, null=True)
email = models.CharField('电子邮箱', max_length=50, blank=True, null=True)
ratepayer_number = models.CharField('纳税人识别号码', max_length=50, blank=True, null=True)
bank = models.CharField('开户银行', max_length=50, blank=True, null=True)
account_number = models.CharField('银行账号', max_length=50, blank=True, null=True)
nation = models.CharField('国家', max_length=50, blank=True, null=True)
province = models.CharField('省份', max_length=50, blank=True, null=True)
city = models.CharField('城市', max_length=50, blank=True, null=True)
address = models.CharField('详细地址', max_length=50, blank=True, null=True)
remark = models.CharField('备注', max_length=512, blank=True, null=True)
init_receivable = models.DecimalField('初期应收', max_digits=20, decimal_places=2, blank=True, null=True) # 精确到小数点后两位
current_receivable = models.DecimalField('末期应收', max_digits=20, decimal_places=2, blank=True, null=True) # 精确到小数点后两位
order_number = models.IntegerField('排序号码', default=100)
class Meta:
db_table = 't_customer'
verbose_name = '客户'
verbose_name_plural = verbose_name
ordering = ['order_number', 'id']
def __str__(self):
return self.name
# 仓库模型类
class WarehouseModel(BaseModel):
name = models.CharField(max_length=100, verbose_name='仓库名称', unique=True)
nation = models.CharField('国家', max_length=50, blank=True, null=True)
province = models.CharField('省份', max_length=50, blank=True, null=True)
city = models.CharField('城市', max_length=50, blank=True, null=True)
address = models.CharField('详细地址', max_length=50, blank=True, null=True)
remark = models.CharField('备注', max_length=512, blank=True, null=True)
warehouse_fee = models.DecimalField('仓储费用(元/天/KG)', max_digits=10, decimal_places=2, blank=True, null=True) # 精确到小数点后两位
truckage = models.DecimalField('搬运费用', max_digits=10, decimal_places=2, blank=True, null=True) # 精确到小数点后两位
order_number = models.IntegerField('排序号码', default=100)
is_default = models.BooleanField('是否默认的仓库', default=False)
# 用户模型来自于erp_system的APP中必须要加前缀
leader_user = models.ForeignKey('erp_system.UserModel', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='仓库负责人')
class Meta:
db_table = 't_warehouse'
verbose_name = '仓库'
verbose_name_plural = verbose_name
ordering = ['order_number', 'id']
def __str__(self):
return self.name
# 结算账户模型类
class SettlementAccountModel(BaseModel):
name = models.CharField(max_length=100, verbose_name='仓库名称', unique=True)
number_code = models.CharField('编号', max_length=28, unique=True)
remark = models.CharField('备注', max_length=512, blank=True, null=True)
init_amount = models.DecimalField('初期金额', max_digits=10, decimal_places=2, blank=True, null=True) # 精确到小数点后两位
balance = models.DecimalField('余额', max_digits=10, decimal_places=2, blank=True, null=True) # 精确到小数点后两位
order_number = models.IntegerField('排序号码', default=100)
is_default = models.BooleanField('是否默认', default=False)
class Meta:
db_table = 't_settlement_account'
verbose_name = '结算账户'
verbose_name_plural = verbose_name
ordering = ['order_number', 'id']
def __str__(self):
return self.name

@ -0,0 +1,29 @@
from rest_framework import serializers
from basic_info.models import Nation, Province, City
class NationSerializer(serializers.ModelSerializer):
"""
国家列化器
"""
class Meta:
model = Nation
fields = '__all__'
class ProvinceSerializer(serializers.ModelSerializer):
"""
省份列化器
"""
class Meta:
model = Province
fields = '__all__'
class CitySerializer(serializers.ModelSerializer):
"""
城市列化器
"""
class Meta:
model = City
fields = '__all__'

@ -0,0 +1,24 @@
from rest_framework import serializers
from basic_info.models import CustomerModel
class CustomerSerializer(serializers.ModelSerializer):
"""
客户信息的序列化器
"""
class Meta:
model = CustomerModel
fields = '__all__'
class CustomerFindSerializer(serializers.ModelSerializer):
"""
搜索过滤的序列化器
"""
name = serializers.CharField(required=False, write_only=True, help_text='客户名称关键字')
class Meta:
model = CustomerModel
fields = ['name', 'mobile', 'phone']

@ -0,0 +1,12 @@
from basic_info.models import SettlementAccountModel
from rest_framework import serializers
class AccountSerializer(serializers.ModelSerializer):
"""
结算账户信息的序列化器
"""
class Meta:
model = SettlementAccountModel
fields = '__all__'

@ -0,0 +1,24 @@
from rest_framework import serializers
from basic_info.models import SupplierModel
class SupplierSerializer(serializers.ModelSerializer):
"""
供应商信息的序列化器
"""
class Meta:
model = SupplierModel
fields = '__all__'
class SupplierFindSerializer(serializers.ModelSerializer):
"""
搜索过滤的序列化器
"""
name = serializers.CharField(required=False, write_only=True)
class Meta:
model = SupplierModel
fields = ['name', 'mobile', 'phone']

@ -0,0 +1,24 @@
from basic_info.models import WarehouseModel
from rest_framework import serializers
class WarehouseSerializer(serializers.ModelSerializer):
"""
仓库信息的序列化器
"""
class Meta:
model = WarehouseModel
fields = '__all__'
# 用于查询结果的序列化器,因为需要显示负责人的真实名字
class WarehouseSearchSerializer(serializers.ModelSerializer):
"""
查询结果的序列化, SlugRelatedField,返回主表的某一个字段值:slug_field可以指定一个需要展示的字段
"""
leader_user = serializers.SlugRelatedField(slug_field='real_name', read_only=True)
class Meta:
model = WarehouseModel
fields = '__all__'

@ -0,0 +1,31 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from basic_info.models import WarehouseModel
from goods_info.models import GoodsModel, GoodsInventoryModel
import logging
logger = logging.getLogger('my')
@receiver(post_save, sender=WarehouseModel)
def create_goods_inventory(sender, instance, created, **kwargs):
"""
创建信号监控函数
创建新仓库之后 给所有的货品在当前的新仓库 新增库存数据
"""
if created:
logger.info('创建当前的新仓库——新增库存数据')
if isinstance(instance, WarehouseModel):
# 首先查询所有的货品id
ids = GoodsModel.objects.values_list('id', flat=True).all()
inventory_list = []
for gid in ids:
inventory_list.append(GoodsInventoryModel(
goods_id=gid,
warehouse_name=instance.name,
warehouse=instance
))
# 一定要采用批量新增函数bulk_create
GoodsInventoryModel.objects.bulk_create(inventory_list)
else:
logger.info('不是WarehouseModel类型所以不需要创建库存数据')

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,39 @@
"""ERP_5 URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, re_path
from rest_framework.routers import DefaultRouter
from .views import supplier_views, customer_views, area_viewsets, settlement_account_views, warehouse_views
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
# path('admin/', admin.site.urls),
# path('users/login/', obtain_jwt_token), # 登录并且由JWT签发token
# re_path(r'^users/reset/(?P<pk>\d+)/$', user_views.ResetPasswordView.as_view()),
]
router = DefaultRouter()
router.register('suppliers', supplier_views.SupplierViewSet)
router.register('customers', customer_views.CustomerViewSet)
router.register(r'nation', area_viewsets.NationView) # 国家的视图接口
router.register(r'province', area_viewsets.ProvinceView) # 省份的视图接口
router.register(r'city', area_viewsets.CityView) # 城市的视图接口
router.register(r'warehouses', warehouse_views.WarehouseView) #
router.register(r'accounts', settlement_account_views.AccountView) #
urlpatterns += router.urls

@ -0,0 +1,154 @@
import logging
from rest_framework import viewsets
from ERP_5.utils.base_views import MultipleDestroyMixin
from basic_info.models import Nation, Province, City
from basic_info.serializer.area_serializer import NationSerializer, ProvinceSerializer, CitySerializer
logger = logging.getLogger('my')
class NationView(viewsets.ModelViewSet, MultipleDestroyMixin):
"""
create:
国家--新增
国家新增, status: 201(成功), return: 新增国家信息
destroy:
国家--删除
国家删除, status: 204(成功), return: None
multiple_delete:
国家--批量删除,必传参数ids=[1,2,3,4...]
国家批量删除, status: 204(成功), return: None
update:
国家--修改,只是修改国家名字
国家修改, status: 200(成功), return: 修改后的国家信息
partial_update:
国家--局部修改,只是修改国家名字
国家局部修改, status: 200(成功), return: 修改后的国家信息
list:
国家--获取列表
国家列表信息, status: 200(成功), return: 国家信息列表
retrieve:
查询某一个国家
查询指定ID的国家, status: 200(成功), return: 用户国家
"""
queryset = Nation.objects.all()
serializer_class = NationSerializer
class ProvinceView(viewsets.ModelViewSet, MultipleDestroyMixin):
"""
create:
省份--新增
省份新增, status: 201(成功), return: 新增省份信息
destroy:
省份--删除
省份删除, status: 204(成功), return: None
multiple_delete:
省份--批量删除,必传参数ids=[1,2,3,4...]
省份批量删除, status: 204(成功), return: None
update:
省份--修改,只是修改省份名字
省份修改, status: 200(成功), return: 修改后的省份信息
partial_update:
省份--局部修改,只是修改省份名字
省份局部修改, status: 200(成功), return: 修改后的省份信息
list:
省份--获取列表请求参数为nid(国家ID), 没有传参数表示所有省份列表nid=x 表示查询该国家的省份列表
省份列表信息, status: 200(成功), return: 省份信息列表
retrieve:
查询某一个省份
查询指定ID的省份, status: 200(成功), return: 用户省份
"""
queryset = Province.objects.all()
serializer_class = ProvinceSerializer
def get_queryset(self):
# 请求参数为nid(国家ID), 没有传参数表示所有省份列表。nid=x 表示查询该国家的省份列表
nid = self.request.query_params.get('nid', None)
if nid:
nid = int(nid)
return Province.objects.filter(nation__id=nid).all()
else:
return Province.objects.all()
class CityView(viewsets.ModelViewSet, MultipleDestroyMixin):
"""
create:
城市--新增
城市新增, status: 201(成功), return: 新增城市信息
destroy:
城市--删除
城市删除, status: 204(成功), return: None
multiple_delete:
城市--批量删除,必传参数ids=[1,2,3,4...]
城市批量删除, status: 204(成功), return: None
update:
城市--修改,只是修改城市名字
城市修改, status: 200(成功), return: 修改后的城市信息
partial_update:
城市--局部修改,只是修改城市名字
城市局部修改, status: 200(成功), return: 修改后的城市信息
list:
城市--获取列表请求参数为pid(省份ID), 没有传参数表示所有城市列表pid=x 表示查询该省份下的城市列表
城市列表信息, status: 200(成功), return: 城市信息列表
retrieve:
查询某一个城市
查询指定ID的城市, status: 200(成功), return: 用户城市
"""
queryset = City.objects.all()
serializer_class = CitySerializer
def get_queryset(self):
# 请求参数为pid(省份ID), 没有传参数表示所有城市列表。pid=x 表示查询该省份下的城市列表
pid = self.request.query_params.get('pid', None)
if pid:
pid = int(pid)
return City.objects.filter(province__id=pid).all()
else:
return City.objects.all()

@ -0,0 +1,88 @@
from django.db.models import Q
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.viewsets import ModelViewSet
from ERP_5.utils.base_views import MultipleDestroyMixin, MultipleOpenMixin
from ERP_5.utils.paginations import GlobalPagination
from basic_info.models import CustomerModel
from basic_info.serializer.customer_serializer import CustomerSerializer, CustomerFindSerializer
class CustomerViewSet(ModelViewSet, MultipleDestroyMixin, MultipleOpenMixin):
"""
create:
客户信息--新增
客户信息新增, status: 201(成功), return: 新增客户信息信息
destroy:
客户信息--删除
客户信息删除, status: 204(成功), return: None
multiple_delete:
客户信息--批量删除,必传参数ids=[1,2,3,4...]
客户信息批量删除, status: 204(成功), return: None
multiple_open:
客户信息--批量启用或者禁用,必传(json)参数ids=[1,2,3,4...](列表中可以只有一个)is_open=1/0
{
"ids":[1,2],
"is_open":"0"
}
is_open=1表示禁用is_open=0表示启用
客户信息批量启用或者禁用, status: 204(成功), return: None
update:
客户信息--修改,
客户信息修改, status: 200(成功), return: 修改后的客户信息信息
partial_update:
客户信息--局部修改,可以传参任意属性的值服务器会修改指定的属性值
客户信息局部修改, status: 200(成功), return: 修改后的客户信息信息
find:
客户信息--获取分页列表可选json参数:name(名称)mobile(手机号码)phone(联系电话)
{
"name":"长沙",
"mobile":"186","phone":xxx
}
客户信息列表信息, status: 200(成功), return: 客户信息信息列表
retrieve:
查询某一个客户信息
查询指定ID的客户信息, status: 200(成功), return: 用户客户信息
"""
queryset = CustomerModel.objects.all()
serializer_class = CustomerSerializer
pagination_class = GlobalPagination
def get_queryset(self):
if self.action == 'find': # 过滤查询
# 获取请求中的数据
name = self.request.data.get('name', None)
phone = self.request.data.get('phone', None)
mobile = self.request.data.get('mobile', None)
query = Q()
if name:
query.add(Q(name__contains=name), 'AND') # 多条件组合
if phone:
query.add(Q(phone__contains=phone), 'AND')
if mobile:
query.add(Q(mobile__contains=mobile), 'AND')
return CustomerModel.objects.filter(query).all()
else:
return CustomerModel.objects.all()
@swagger_auto_schema(method='POST', request_body=CustomerFindSerializer, operation_description="查询过滤")
@action(methods=['POST'], detail=False)
def find(self, request, *args, **kwargs):
return super(CustomerViewSet, self).list(request, args, kwargs)

@ -0,0 +1,96 @@
import logging
from django.db.models import Q
from django.utils.decorators import method_decorator
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import viewsets
from basic_info.models import SettlementAccountModel
from ERP_5.apps.basic_info.serializer.settlement_account_serializer import AccountSerializer
from ERP_5.utils.paginations import GlobalPagination
from ERP_5.utils.base_views import MultipleDestroyMixin, MultipleOpenMixin
logger = logging.getLogger('my')
param1 = openapi.Parameter(name='name', type=openapi.TYPE_STRING, description="名称的关键字", in_=openapi.IN_QUERY)
param2 = openapi.Parameter(name='remark', type=openapi.TYPE_STRING, description="备注的关键字", in_=openapi.IN_QUERY)
param3 = openapi.Parameter(name='number_code', type=openapi.TYPE_STRING, description="编号关键字", in_=openapi.IN_QUERY)
@method_decorator(name='list', decorator=swagger_auto_schema(
manual_parameters=[param1, param2, param3],
operation_description="账户的搜索过滤")
)
class AccountView(viewsets.ModelViewSet, MultipleDestroyMixin, MultipleOpenMixin):
"""
create:
结算账户信息--新增
结算账户信息新增, status: 201(成功), return: 新增结算账户信息信息
destroy:
结算账户信息--删除
结算账户信息删除, status: 204(成功), return: None
multiple_delete:
结算账户信息--批量删除,必传json参数ids=[1,2,3,4...]
结算账户信息批量删除, status: 204(成功), return: None
multiple_open:
结算账户信息--批量启用或者禁用,必传(json)参数ids=[1,2,3,4...](列表中可以只有一个)is_open=1/0
{
"ids":[1,2],
"is_open":"0"
}
is_open=1表示禁用is_open=0表示启用
结算账户信息批量启用或者禁用, status: 204(成功), return: None
update:
结算账户信息--修改,一般用于修改多个属性
结算账户信息修改, status: 200(成功), return: 修改后的结算账户信息信息
partial_update:
结算账户信息--局部修改(一般修改一个属性),可以传参任意属性的值服务器会修改指定的属性值
结算账户信息局部修改, status: 200(成功), return: 修改后的结算账户信息信息
list:
结算账户信息--获取分页列表可选json参数:name(名称)remark(描述关键字),number_code(编号)
{
"name":"长沙",
"remark":"xxx", "number_code":"xxxx"
}
结算账户信息列表信息, status: 200(成功), return: 结算账户信息信息列表
retrieve:
查询某一个结算账户信息
查询指定ID的结算账户信息, status: 200(成功), return: 用户结算账户信息
"""
queryset = SettlementAccountModel.objects.all()
serializer_class = AccountSerializer
pagination_class = GlobalPagination
def get_queryset(self):
if self.action == 'list': # 过滤查询
# 获取请求参数(在json中)namephone, mobile
name = self.request.data.get('name', None)
remark = self.request.data.get('remark', None)
number_code = self.request.data.get('number_code', None)
query = Q()
if name:
query.add(Q(name__contains=name), 'AND') # 多条件组合
if remark:
query.add(Q(remark__contains=remark), 'AND')
if remark:
query.add(Q(number_code__contains=number_code), 'AND')
return SettlementAccountModel.objects.filter(query).all()
else:
return SettlementAccountModel.objects.all()

@ -0,0 +1,91 @@
from django.db.models import Q
from django.utils.decorators import method_decorator
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework.viewsets import ModelViewSet
from ERP_5.utils.base_views import MultipleDestroyMixin, MultipleOpenMixin
from ERP_5.utils.paginations import GlobalPagination
from basic_info.models import SupplierModel
from basic_info.serializer.supplier_serializer import SupplierSerializer, SupplierFindSerializer
param1 = openapi.Parameter(name='name', type=openapi.TYPE_STRING, description="供应商名称的关键字", in_=openapi.IN_QUERY)
param2 = openapi.Parameter(name='mobile', type=openapi.TYPE_STRING, description="供应商的手机号码", in_=openapi.IN_QUERY)
param3 = openapi.Parameter(name='phone', type=openapi.TYPE_STRING, description="供应商名称的联系电话", in_=openapi.IN_QUERY)
@method_decorator(name='list', decorator=swagger_auto_schema(
manual_parameters=[param1, param2, param3],
operation_description="供应商的搜索过滤")
)
class SupplierViewSet(ModelViewSet, MultipleDestroyMixin, MultipleOpenMixin):
"""
create:
供应商信息--新增
供应商信息新增, status: 201(成功), return: 新增供应商信息信息
destroy:
供应商信息--删除
供应商信息删除, status: 204(成功), return: None
multiple_delete:
供应商信息--批量删除,必传参数ids=[1,2,3,4...]
供应商信息批量删除, status: 204(成功), return: None
multiple_open:
供应商信息--批量启用或者禁用,必传(json)参数ids=[1,2,3,4...](列表中可以只有一个)is_open=1/0
{
"ids":[1,2],
"is_open":"0"
}
is_open=1表示禁用is_open=0表示启用
供应商信息批量启用或者禁用, status: 204(成功), return: None
update:
供应商信息--修改,
供应商信息修改, status: 200(成功), return: 修改后的供应商信息信息
partial_update:
供应商信息--局部修改,可以传参任意属性的值服务器会修改指定的属性值
供应商信息局部修改, status: 200(成功), return: 修改后的供应商信息信息
list:
供应商信息--搜索过滤的分页列表可选json参数:name(名称)mobile(手机号码)phone(联系电话)
{
"name":"长沙",
"mobile":"186","phone":xxx
}
供应商信息列表信息, status: 200(成功), return: 供应商信息信息列表
retrieve:
查询某一个供应商信息
查询指定ID的供应商信息, status: 200(成功), return: 用户供应商信息
"""
pagination_class = GlobalPagination
queryset = SupplierModel.objects.all()
serializer_class = SupplierSerializer
def get_queryset(self):
if self.action == 'list': # 过滤查询
# 获取请求中的数据
name = self.request.query_params.get('name', None)
phone = self.request.query_params.get('phone', None)
mobile = self.request.query_params.get('mobile', None)
query = Q()
if name:
query.add(Q(name__contains=name), 'AND') # 多条件组合
if phone:
query.add(Q(phone__contains=phone), 'AND')
if mobile:
query.add(Q(mobile__contains=mobile), 'AND')
return SupplierModel.objects.filter(query).all()
else:
return SupplierModel.objects.all()

@ -0,0 +1,98 @@
import logging
from django.db.models import Q
from django.utils.decorators import method_decorator
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import viewsets
from basic_info.models import WarehouseModel
from ERP_5.apps.basic_info.serializer.warehouse_serializer import WarehouseSerializer, WarehouseSearchSerializer
from ERP_5.utils.paginations import GlobalPagination
from ERP_5.utils.base_views import MultipleDestroyMixin, MultipleOpenMixin
logger = logging.getLogger('my')
param1 = openapi.Parameter(name='name', type=openapi.TYPE_STRING, description="名称的关键字", in_=openapi.IN_QUERY)
param2 = openapi.Parameter(name='remark', type=openapi.TYPE_STRING, description="备注关键字", in_=openapi.IN_QUERY)
@method_decorator(name='list', decorator=swagger_auto_schema(
manual_parameters=[param1, param2],
operation_description="仓库的搜索过滤")
)
class WarehouseView(viewsets.ModelViewSet, MultipleDestroyMixin, MultipleOpenMixin):
"""
create:
仓库信息--新增
仓库信息新增, status: 201(成功), return: 新增仓库信息信息
destroy:
仓库信息--删除
仓库信息删除, status: 204(成功), return: None
multiple_delete:
仓库信息--批量删除,必传json参数ids=[1,2,3,4...]
仓库信息批量删除, status: 204(成功), return: None
multiple_open:
仓库信息--批量启用或者禁用,必传(json)参数ids=[1,2,3,4...](列表中可以只有一个)is_open=1/0
{
"ids":[1,2],
"is_open":"0"
}
is_open=1表示禁用is_open=0表示启用
仓库信息批量启用或者禁用, status: 204(成功), return: None
update:
仓库信息--修改,一般用于修改多个属性
仓库信息修改, status: 200(成功), return: 修改后的仓库信息信息
partial_update:
仓库信息--局部修改(一般修改一个属性),可以传参任意属性的值服务器会修改指定的属性值
仓库信息局部修改, status: 200(成功), return: 修改后的仓库信息信息
list:
仓库信息--获取分页列表可选json参数:name(名称)remark(描述关键字)
{
"name":"长沙",
"remark":"xxxx"
}
仓库信息列表信息, status: 200(成功), return: 仓库信息信息列表
retrieve:
查询某一个仓库信息
查询指定ID的仓库信息, status: 200(成功), return: 用户仓库信息
"""
queryset = WarehouseModel.objects.all()
serializer_class = WarehouseSerializer
pagination_class = GlobalPagination
def get_queryset(self):
if self.action == 'list': # 过滤查询
# 获取请求参数(在json中)namephone, mobile
name = self.request.data.get('name', None)
remark = self.request.data.get('remark', None)
query = Q()
if name:
query.add(Q(name__contains=name), 'AND') # 多条件组合
if remark:
query.add(Q(remark__contains=remark), 'AND')
return WarehouseModel.objects.filter(query).all()
else:
return WarehouseModel.objects.all()
def get_serializer_class(self):
if self.action == 'list': # 过滤查询 ,设置新的序列化器
return WarehouseSearchSerializer
else:
return WarehouseSerializer

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,9 @@
from django.apps import AppConfig
class ErpSystemConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'erp_system'
#
# def ready(self):
# import erp_system.signals

@ -0,0 +1,28 @@
import logging
from django.contrib.auth.backends import ModelBackend
from ERP_5.utils.cache_permissions import cache_permissions_by_user
from erp_system.models import UserModel
logger = logging.getLogger('my')
class UserLoginAuth(ModelBackend):
"""
重新用户登录认证
"""
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = UserModel.objects.get(username=username)
except Exception as ex:
logger.error(ex)
return None
# 判断密码
if user and user.check_password(password):
# 缓存用户的权限
cache_permissions_by_user(user)
return user
return None

@ -0,0 +1,111 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from ERP_5.utils.base_model import BaseModel
class Menu(BaseModel):
"""菜单功能的模型类"""
number = models.IntegerField('排序数字', blank=True, null=True)
url = models.CharField('菜单访问的URL', max_length=256, blank=True, null=True)
name = models.CharField('菜单名字', max_length=50)
# 父菜单对象parent
parent = models.ForeignKey('self', blank=True, null=True, related_name='children', on_delete=models.CASCADE)
class Meta:
db_table = 't_menu'
verbose_name = '功能菜单'
verbose_name_plural = verbose_name
ordering = ['number']
def __str__(self):
return self.name
# ERP系统的操作用户模型类默认是采用django自带的User需要配置settings
class UserModel(AbstractUser):
phone = models.CharField('手机号码', max_length=11, unique=True, blank=True, null=True)
real_name = models.CharField('真实姓名', max_length=128, blank=True, null=True)
roles = models.ManyToManyField('RolesModel', db_table='t_users_roles', blank=True, verbose_name='用户所拥有的角色列表')
dept = models.ForeignKey('DeptModel', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='用户所在的部门')
class Meta:
db_table = 't_user'
verbose_name = '功能菜单'
verbose_name_plural = verbose_name
ordering = ['id']
def __str__(self):
return self.username
class PermissionsModel(BaseModel):
"""
权限模型
"""
method_choices = (
('POST', ''),
('DELETE', ''),
('PUT', ''),
('PATCH', '改局部'),
('GET', '')
)
"""
只要是choices参数的字段 如果你想要获取对应信息 固定写法 get_字段名_display()
print(permission.method)
print(permission.get_method_display())
"""
name = models.CharField(max_length=50, verbose_name='权限名')
is_menu = models.BooleanField(verbose_name='是否为菜单', default=True) # True为菜单,False为接口
# 操作 , 如果是菜单(非接口),method
method = models.CharField(max_length=8, blank=True, default='', choices=method_choices, verbose_name='请求方法')
path = models.CharField(max_length=200, blank=True, default='', verbose_name='请求路径')
# 资源
menu = models.ForeignKey('Menu', null=True, blank=True, related_name='permissions_list', on_delete=models.CASCADE)
desc = models.CharField(max_length=512, blank=True, default='', verbose_name='权限描述')
def __str__(self):
return self.name
class Meta:
db_table = 't_permissions'
verbose_name = '权限'
verbose_name_plural = verbose_name
ordering = ['id']
class RolesModel(BaseModel):
"""
角色
"""
name = models.CharField(max_length=32, unique=True, verbose_name='角色名字')
permissions = models.ManyToManyField('PermissionsModel', db_table='t_roles_permissions',
blank=True, verbose_name='权限')
# 注意在用户模型中最好也要加ManyToManyField
def __str__(self):
return self.name
class Meta:
db_table = 't_roles'
verbose_name = '角色'
verbose_name_plural = verbose_name
class DeptModel(BaseModel):
"""
组织架构 部门
"""
name = models.CharField(max_length=32, unique=True, verbose_name='部门')
parent = models.ForeignKey('self', null=True, blank=True, related_name='children', on_delete=models.CASCADE, verbose_name='父部门')
address = models.CharField(max_length=128, blank=True, default='', verbose_name='部门地址')
def __str__(self):
return self.name
class Meta:
db_table = 't_dept'
verbose_name = '部门'
verbose_name_plural = verbose_name

@ -0,0 +1,27 @@
from rest_framework import serializers
from erp_system.models import DeptModel
class ChildDeptSerializer(serializers.ModelSerializer):
children = serializers.SerializerMethodField(read_only=True)
class Meta:
model = DeptModel
fields = ['id', 'name', 'parent', 'children']
def get_children(self, obj):
if obj.children:
return ChildDeptSerializer(instance=obj.children, many=True).data
return None
class DeptSerializer(serializers.ModelSerializer):
"""
部门的序列化器
"""
children = ChildDeptSerializer(read_only=True, many=True)
class Meta:
model = DeptModel
fields = ['id', 'name', 'parent', 'children']

@ -0,0 +1,10 @@
from rest_framework.serializers import ModelSerializer
from erp_system.models import Menu
class MenuSerializer(ModelSerializer):
class Meta:
model = Menu
fields = '__all__'

@ -0,0 +1,9 @@
from rest_framework.serializers import ModelSerializer, ValidationError
from erp_system.models import PermissionsModel
# 权限基本的序列化类
class PermissionsSerializer(ModelSerializer):
class Meta:
model = PermissionsModel
fields = '__all__'

@ -0,0 +1,53 @@
from rest_framework.serializers import ModelSerializer, Serializer, IntegerField, BooleanField
from .permission_serializer import PermissionsSerializer
from erp_system.models import RolesModel, PermissionsModel
from rest_framework import serializers
class RolesSerializer(ModelSerializer):
"""
普通的序列化
"""
permissions = PermissionsSerializer(many=True, read_only=True)
delete_flag = serializers.CharField(read_only=True)
class Meta:
model = RolesModel
fields = '__all__'
class RolesPartialSerializer(ModelSerializer):
"""
给一个角色批量授权的序列化器
"""
class Meta:
model = RolesModel
fields = ['id', 'permissions']
def save(self, **kwargs):
# 针对某一个角色批量授权,之前存在的不变,之前没有的会增加权限
if self.instance:
permission_list = self.validated_data.get('permissions')
current_list = self.instance.permissions.all()
if permission_list:
for p in permission_list:
if p not in current_list:
# 多对多的关联 如果数据库中已经存在对象则使用add否则使用
# 变量名=变量名.B类名小写_set.create(字段='值')
self.instance.permissions.add(p.id)
self.instance.save()
return self.instance
class RoleSetPermissionSerializer(Serializer):
"""
角色单一授权或者取消单一授权
"""
# 角色ID
role_id = IntegerField(write_only=True, required=True, help_text='选择的角色ID')
permission_id = IntegerField(write_only=True, required=True, help_text='选择的权限ID')
# 是否是:取消授权还是授予权限
is_create = BooleanField(write_only=True, required=True, help_text='是否授予还是删除权限')

@ -0,0 +1,101 @@
from rest_framework.serializers import ModelSerializer, ValidationError
from rest_framework import serializers
from erp_system.models import UserModel
import re
# 用户注册基本的序列化类
from erp_system.serializers.roles_serialzier import RolesSerializer
class UserAddSerializer(ModelSerializer):
"""
仅仅用于用户注册
"""
class Meta:
model = UserModel
fields = ('id', 'username', 'password', 'phone', 'real_name')
extra_kwargs = {
'username': {
'max_length': 12,
'min_length': 2
},
'password': {
'max_length': 8,
'min_length': 3,
'write_only': True
},
}
def validate_phone(self, phone):
"""
自定义验证手机号码规则函数名=validate_<field_name>
"""
if not re.match(r'^1[3589]\d{9}$', phone):
raise ValidationError("请输入正确的手机号码")
return phone
def create(self, validated_data):
"""重写用户注册,否则直接把明文密码保存到数据库"""
user = UserModel.objects.create_user(**validated_data)
return user
from rest_framework.serializers import ModelSerializer, ValidationError, CharField, ValidationError
class UserUpdateDeleteSerializer(ModelSerializer):
"""
只用于修改用户修改用户的角色修改用户的部门和删除用户
"""
class Meta:
model = UserModel
fields = ('id', 'roles', 'dept')
class ResetPasswordSerializer(ModelSerializer):
"""
重置密码序列化器,只用与修改密码
"""
confirm_password = CharField(write_only=True)
class Meta:
model = UserModel
fields = ['id', 'password', 'confirm_password']
extra_kwargs = {
'password': {
'write_only': True
}
}
def validate(self, attrs):
# partial_update, 局部更新required验证无效, 手动验证数据
password = attrs.get('password')
confirm_password = attrs.get('confirm_password')
if not password:
raise ValidationError('字段password为必填项')
if not confirm_password:
raise ValidationError('字段confirm_password为必填项')
if password != confirm_password:
raise ValidationError('两次密码不一致')
return attrs
def save(self, *args):
# 重写save方法, 保存密码
self.instance.set_password(self.validated_data.get('password'))
self.instance.save()
return self.instance
class UserGetSerializer(ModelSerializer):
"""
只用于查询用户
"""
roles = RolesSerializer(many=True, read_only=True)
dept_name = serializers.CharField(source='dept.name')
class Meta:
model = UserModel
fields = ('id', 'username', 'phone', 'real_name', 'roles', 'dept_name')

@ -0,0 +1,45 @@
# import logging
#
# from django.dispatch import receiver
# from django.db.models.signals import post_save
#
# from erp_system.models import Menu, PermissionsModel
#
# # 默认情况下,每一个功能菜单的接口的请求方法数量
# methods = {'POST': '新增', 'GET': '查询', 'PUT': '修改', 'DELETE': '删除', 'PATCH': '局部修改'}
#
# logger = logging.getLogger('my')
#
#
# @receiver(post_save, sender=Menu)
# def create_menus_permissions(sender, instance, created, **kwargs):
# """
# 当有人新增一个功能菜单Menu)之后, 需要把该功能菜单对应的权限也插入到数据库
# :param sender:
# :param instance:
# :param created:
# :param kwargs:
# :return:
# """
#
# if created:
# logger.info('Menu对象已经新增完成了')
# if isinstance(instance, Menu):
# if not instance.parent: # 表示是一个顶级菜单没有请求URL地址不是一个接口
# permission = PermissionsModel.objects.create(
# name=instance.name + "的权限",
# is_menu=True,
# menu=instance
# )
# else: # 表示是接口有父菜单有请求的url地址
# for method in methods:
# permission = PermissionsModel.objects.create(
# name=methods.get(method) + "的权限",
# is_menu=False,
# menu=instance,
# method=method,
# path=instance.url,
# desc=f'{instance.name}的{methods.get(method)}的权限'
# )
# else:
# logger.info('instance不是Menu类型所以不需要任何处理')

@ -0,0 +1,35 @@
import logging
from celery import shared_task
from ERP_5.utils.tasks_hook import HookTask
from erp_system.models import PermissionsModel, Menu
# 默认情况下,每一个功能菜单的接口的请求方法数量
methods = {'POST': '新增', 'GET': '查询', 'PUT': '修改', 'DELETE': '删除', 'PATCH': '局部修改'}
logger = logging.getLogger('my')
# 在装饰器中,指定一个任务钩子
@shared_task(base=HookTask)
def create_menus_permissions(menu_id): # 定义了一个任务
logger.info('Menu对象已经新增完成了')
instance = Menu.objects.get(pk=menu_id)
if not instance.parent: # 表示是一个顶级菜单没有请求URL地址不是一个接口
permission = PermissionsModel.objects.create(
name=instance.name + "的权限",
is_menu=True,
menu=instance
)
else: # 表示是接口有父菜单有请求的url地址
for method in methods:
permission = PermissionsModel.objects.create(
name=methods.get(method) + "的权限",
is_menu=False,
menu=instance,
method=method,
path=instance.url,
desc=f'{instance.name}{methods.get(method)}的权限'
)

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,39 @@
"""ERP_5 URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, re_path
from rest_framework.routers import DefaultRouter
from .views import menu_views, user_views, dept_views, role_views, permission_views
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
# path('admin/', admin.site.urls),
path('users/login/', obtain_jwt_token), # 登录并且由JWT签发token
re_path(r'^users/reset/(?P<pk>\d+)/$', user_views.ResetPasswordView.as_view()),
]
router = DefaultRouter()
router.register('menus', menu_views.MenuViewSet)
router.register('users/register', user_views.RegisterUserView)
router.register('roles', role_views.RolesViewSet)
router.register('permissions', permission_views.PermissionViewSet)
router.register('dept', dept_views.DeptViewSet)
router.register('users', user_views.UserView)
urlpatterns += router.urls

@ -0,0 +1,83 @@
from django.utils.decorators import method_decorator
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet
from ERP_5.utils.base_views import MultipleDestroyMixin
from ERP_5.utils.paginations import GlobalPagination
from erp_system.models import DeptModel
from erp_system.serializers.dept_serializer import DeptSerializer
# 定义好了一个swagger的参数
pid_param = openapi.Parameter(name='pid', in_=openapi.IN_QUERY, description='父部门的ID', required=False,
type=openapi.TYPE_INTEGER)
@method_decorator(name='list', decorator=swagger_auto_schema(
manual_parameters=[pid_param],
operation_description="查询部门的树形结构数据, pid=0或者不传pid表示查询顶级机构的树。")
)
class DeptViewSet(ModelViewSet, MultipleDestroyMixin):
"""
create:
部门机构--新增
部门机构新增, status: 201(成功), return: 新增部门机构信息
destroy:
部门机构--删除
部门机构删除, status: 204(成功), return: None
multiple_delete:
部门机构--批量删除,必传参数ids=[1,2,3,4...]
部门机构批量删除, status: 204(成功), return: None
update:
部门机构--修改,不能修改部门机构只是修改部门机构名字
部门机构修改, status: 200(成功), return: 修改后的部门机构信息
partial_update:
部门机构--局部修改(部门机构批量授权),只能真的某一个部门机构一次性授权之前的的授权会被覆盖
部门机构批量授权, status: 200(成功), return: 修改后的部门机构信息
list:
部门机构--获取列表, 如果传参pid=0或者pid不传则查询所有顶级部门列表 如果传参pid=具体的值则查询某个父部门下的所有子部门
部门机构列表信息, status: 200(成功), return: 部门机构信息列表
retrieve:
查询某一个部门机构
查询指定ID的部门机构, status: 200(成功), return: 用户部门机构
"""
def get_queryset(self):
if self.action == 'list':
q = self.request.query_params.get('pid', None)
if q: # 参数中有pid
pid = int(q)
if pid == 0:
return DeptModel.objects.filter(parent__isnull=True).all()
else:
return DeptModel.objects.filter(parent__id=pid).all()
else:
return DeptModel.objects.filter(parent__isnull=True).all()
else:
return DeptModel.objects.all()
queryset = DeptModel.objects.all()
serializer_class = DeptSerializer
pagination_class = GlobalPagination
# permission_classes = [IsAuthenticated]
# @swagger_auto_schema(method='GET', manual_parameters=[pid_param])
# @action(methods=['GET'], detail=False)
# def find(self, request, *args, **kwargs):
# return super(DeptViewSet, self).list(request, *args, **kwargs)

@ -0,0 +1,103 @@
from django.db.models import Q
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from erp_system.tasks import create_menus_permissions
from erp_system.models import Menu, PermissionsModel
from erp_system.serializers.menus_serializer import MenuSerializer
class MenuViewSet(ModelViewSet):
"""
create:
功能菜单--新增
功能菜单新增, status: 201(成功), return: 新增功能菜单信息
destroy:
功能菜单--删除
功能菜单删除, status: 204(成功), return: None
update:
功能菜单--修改,全局修改
功能菜单修改, status: 200(成功), return: 修改后的功能菜单信息
partial_update:
功能菜单--局部修改
功能菜单批修改 status: 200(成功), return: 修改后的功能菜单信息
list:
功能菜单--获取列表
功能菜单列表信息, status: 200(成功), return: 功能菜单信息列表
retrieve:
查询某一个功能菜单
查询指定ID的功能菜单, status: 200(成功), return: 用户功能菜单
find_parent:
查询所有的顶级功能菜单 不需要传参
get_menus_by_permission:
查询当前登录的用户所有的拥有GET权限的菜单列表而且这些菜单列表必须是树形结构
返回树形菜单列表, status: 200(成功)
"""
serializer_class = MenuSerializer
queryset = Menu.objects.all()
# 必须是登录成功之后才能访问所有功能菜单的接口
# permission_classes = [IsAuthenticated]
@action(methods=['get'], detail=False)
def find_parent(self, request, *args, **kwargs):
lst = self.get_queryset().filter(parent__isnull=True)
ser = self.get_serializer(instance=lst, many=True)
return Response(ser.data)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
menu = serializer.save()
# 发布异步的任务
if menu and menu.id:
create_menus_permissions.delay(menu.id)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
@action(methods=['get'], detail=False)
def get_menus_by_permission(self, request, *args, **kwargs):
# 得到当前登录用户
user = request.user
# 查询该用户所拥有的权限ID列表
permission_id = user.roles.values_list('permissions', flat=True).distinct()
# 过滤那些GET的或者是顶级菜单权限
menu_ids = PermissionsModel.objects.filter(Q(method='GET') | Q(is_menu=True)).filter(id__in=permission_id)\
.values_list('menu', flat=True)
# 根据menu_id得到菜单接口对象
menu_list = Menu.objects.filter(id__in=menu_ids, delete_flag=0).all()
serializer = MenuSerializer(instance=menu_list, many=True)
# 定义 Element
tree_dict = {} # 代表一个父菜单
tree_data = []
try:
for item in serializer.data:
tree_dict[item['id']] = item
for i in tree_dict:
if tree_dict[i]['parent']:
pid = tree_dict[i]['parent']
parent = tree_dict[pid]
parent.setdefault('children', []).append(tree_dict[i])
else:
tree_data.append(tree_dict[i])
results = tree_data
except KeyError:
results = serializer.data
return Response(results)

@ -0,0 +1,125 @@
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from erp_system.models import PermissionsModel, RolesModel
from erp_system.serializers.permission_serializer import PermissionsSerializer
import logging
logger = logging.getLogger('my')
class PermissionViewSet(ModelViewSet):
"""
create:
权限--新增
权限新增, status: 201(成功), return: 新增权限信息
destroy:
权限--删除
权限删除, status: 204(成功), return: None
multiple_delete:
权限--批量删除,必传参数ids=[1,2,3,4...]
权限批量删除, status: 204(成功), return: None
update:
权限--修改
权限修改, status: 200(成功), return: 修改增权限信息
partial_update:
权限--局部修改
权限局部修改, status: 200(成功), return: 修改增权限信息
list:
权限--获取所有的权限
find_permissions_by_menu:
获取当前菜单的所有权限设置 列表,必传参数menu_id
例如http://127.0.0.1:8000/permissions/find_permissions_by_menu/?menu_id=2
权限列表信息, status: 200(成功), return: 权限信息列表
find_permissions:
打开授权页面的时候首先请求的接口获取当前角色已经授予的权限ID列表和整个项目的所有权限(树形)列表,必传参数rid(角色ID)
例如http://127.0.0.1:8000/permissions/find_permissions/?rid=2
树形权限列表信息, status: 200(成功), return: 权限信息列表
retrieve:
查询某一个指定的权限信息
查询指定ID的权限, status: 200(成功), return: 权限信息
"""
queryset = PermissionsModel.objects.all()
serializer_class = PermissionsSerializer
# 定义好了一个swagger的参数
menu_param = openapi.Parameter(name='menu_id', in_=openapi.IN_QUERY, description='所选的菜单ID',
type=openapi.TYPE_INTEGER)
@swagger_auto_schema(method='GET', manual_parameters=[menu_param], operation_description='单一授权或者取消单一权限')
@action(methods=['GET'], detail=False)
def find_permissions_by_menu(self, request, *args, **kwargs):
menu_id = request.query_params.get('menu_id')
permission_list = PermissionsModel.objects.filter(menu__id=menu_id).all()
return Response(data=self.get_serializer(instance=permission_list, many=True).data)
# 定义好了一个swagger的参数
menu_param2 = openapi.Parameter(name='rid', in_=openapi.IN_QUERY, description='所选的角色ID',
type=openapi.TYPE_INTEGER)
@swagger_auto_schema(method='GET', manual_parameters=[menu_param2])
@action(methods=['GET'], detail=False)
def find_permissions(self, request, *args, **kwargs):
# 获取当前角色已经授予的权限ID列表和整个项目的所有权限(树形)列表
result = {} # 返回的整个字典数据,
rid = request.query_params.get('rid', None)
if rid: # 查询当前角色的所有已经授权的ID
ids = RolesModel.objects.filter(id=rid).values_list('permissions', flat=True).distinct()
result['ids'] = ids # 存入返回字典中
# 整个ERP系统中所有权限列表而且要树形展示
permissions_list = PermissionsModel.objects.values('id', 'name', 'menu__parent__id', 'menu__name', 'menu__id')
tree_dict = {} # 一个父节点权限(里面是树形嵌套)
tree_data = [] # 所有权限
"""
[
{父节点1的内容children=[{子节点内容}{子节点内容}...]},
{父节点2的内容children=[{子节点内容}{子节点内容}...]},
...
]
"""
# 第一步构建一颗二级树
for item in permissions_list:
tree_dict[item['menu__id']] = item
for i in tree_dict:
if tree_dict[i]['menu__parent__id']: # 表示子权限
pid = tree_dict[i]['menu__parent__id']
parent = tree_dict[pid]
child = dict() # 创建一个空字典(二级)第二级只要menu__id, menu__name, permissions(权限列表)
child['menu__id'] = tree_dict[i]['menu__id']
child['menu__name'] = tree_dict[i]['menu__name']
child.setdefault('permissions', [])
parent.setdefault('children', []).append(child)
else:
tree_data.append(tree_dict[i])
# 第二步:在前面二级树形结构的基础上,添加子权限。得到三级树
for parent in tree_data:
if 'children' in parent:
for child in parent['children']:
for node in permissions_list:
if node['menu__parent__id'] and node['menu__id'] == child['menu__id']:
child['permissions'].append(node)
logger.info(tree_data)
result['tree_data'] = tree_data # 把三级树结构,加入到结果中
return Response(result)

@ -0,0 +1,92 @@
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from erp_system.models import RolesModel, PermissionsModel, Menu
from erp_system.serializers.roles_serialzier import RolesPartialSerializer, RoleSetPermissionSerializer, RolesSerializer
class RolesViewSet(ModelViewSet):
# 整个ERP系统有一个默认存在的角色特殊 admin角色。这个角色不允许删除
"""
create:
角色--新增
角色新增, status: 201(成功), return: 新增角色信息
destroy:
角色--删除
角色删除, status: 204(成功), return: None
multiple_delete:
角色--批量删除,必传参数ids=[1,2,3,4...]
角色批量删除, status: 204(成功), return: None
update:
角色--修改,不能修改角色只是修改角色名字
角色修改, status: 200(成功), return: 修改后的角色信息
partial_update:
角色--(不需要)局部修改(角色批量授权),针对某一个角色批量授权之前存在的不变之前没有的会增加权限
角色批量授权, status: 200(成功), return: 修改后的角色信息
set_permission_to_role:
角色--单个授权一次只能授予角色一个permission也可以取消一个permission
status: 200(成功), return: 修改后的角色信息
list:
角色--获取列表
角色列表信息, status: 200(成功), return: 角色信息列表
retrieve:
查询某一个角色
查询指定ID的角色, status: 200(成功), return: 用户角色
"""
queryset = RolesModel.objects.all()
def get_serializer_class(self):
"""
不同的视图函数采用的序列化器是不一样的
:return:
"""
if self.action == 'partial_update':
return RolesPartialSerializer
elif self.action == 'set_permission_to_role':
return RoleSetPermissionSerializer
else:
return RolesSerializer
@swagger_auto_schema(method='POST', request_body=RoleSetPermissionSerializer, operation_description='单一授权或者取消单一权限')
@action(methods=['POST'], detail=False)
def set_permission_to_role(self, request, *args, **kwargs):
ser: RoleSetPermissionSerializer = self.get_serializer(data=request.data)
if ser.is_valid():
role = RolesModel.objects.get(id=ser.validated_data.get('role_id'))
permission = PermissionsModel.objects.get(id=ser.validated_data.get('permission_id'))
if ser.validated_data.get('is_create'):
# 判断给当前角色新增一个权限, 首先判断该权限所对应的父菜单权限是否已经被授予了
parent = Menu.objects.filter(id=permission.menu.id).values_list('parent', flat=True).all()
if parent:
p_permission = PermissionsModel.objects.get(menu_id=parent[0])
role.permissions.add(p_permission)
role.permissions.add(permission)
else: # 取消权限
role.permissions.remove(permission)
return Response(data=RolesSerializer(instance=role).data)
def destroy(self, request, *args, **kwargs):
if self.get_object().name == 'admin':
return Response(data={'detail': 'admin角色不可删除'}, status=status.HTTP_400_BAD_REQUEST)
else:
pass

@ -0,0 +1,53 @@
from rest_framework import mixins
from rest_framework.generics import GenericAPIView
from rest_framework.viewsets import GenericViewSet
from ERP_5.utils.base_views import MultipleDestroyMixin
from ERP_5.utils.paginations import GlobalPagination
from erp_system.models import UserModel
from erp_system.serializers.user_serializer import UserAddSerializer, UserUpdateDeleteSerializer, UserGetSerializer, \
ResetPasswordSerializer
class RegisterUserView(mixins.CreateModelMixin, GenericViewSet):
"""
create:
用户注册
并且把用户注册之后的用户信息返回
"""
queryset = UserModel.objects.all()
serializer_class = UserAddSerializer
class UserView(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
MultipleDestroyMixin, # 批量删除用户
GenericViewSet):
"""
没有用户注册和修改用户密码的模型类
"""
queryset = UserModel.objects.all()
pagination_class = GlobalPagination
def get_serializer_class(self):
if self.action == 'partial_update' or self.action == 'update' or self.action == 'destroy':
return UserUpdateDeleteSerializer
else:
return UserGetSerializer
class ResetPasswordView(mixins.UpdateModelMixin, GenericAPIView):
"""
patch:
用户--重置密码
用户重置密码, status: 200(成功), return: None
"""
queryset = UserModel.objects.all()
serializer_class = ResetPasswordSerializer
def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,6 @@
from django.apps import AppConfig
class GoodsInfoConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'goods_info'

@ -0,0 +1,120 @@
from django.db import models
from django.db import models
from ERP_5.utils.base_model import BaseModel
# 货品(商品)类别模型类
class GoodsCategoryModel(BaseModel):
name = models.CharField(max_length=100, verbose_name='类别名称')
number_code = models.CharField('编号', max_length=28, unique=True)
remark = models.CharField('备注', max_length=512, blank=True, null=True)
order_number = models.IntegerField('排序号码', default=100)
parent = models.ForeignKey('self', blank=True, null=True, related_name='children', on_delete=models.SET_NULL)
class Meta:
db_table = 't_category'
verbose_name = '商品类别'
verbose_name_plural = verbose_name
ordering = ['order_number', 'id']
def __str__(self):
return self.name
# 计量单位模型类
class UnitsModel(BaseModel):
basic_name = models.CharField(max_length=20, verbose_name='基本单位', unique=True)
backup_name = models.CharField('副单位', max_length=20, null=True, blank=True)
remark = models.CharField('备注', max_length=512, blank=True, null=True)
class Meta:
db_table = 't_units'
verbose_name = '计量单位'
verbose_name_plural = verbose_name
ordering = ['id']
def __str__(self):
return f'{self.basic_name}({self.backup_name})'
# 图片或者附件的模型类
class AttachmentModel(BaseModel):
# 附件文件的类型
type_choices = (
('image', '图片'),
('doc', 'Word文档'),
('excel', 'Excel文档'),
('zip', '压缩文件'),
('other', '其他文件')
)
"""
只要是choices参数的字段 如果你想要获取对应信息 固定写法 get_字段名_display()
print(Attachment.a_type)
print(Attachment.get_a_type_display())
"""
a_file = models.FileField('附件或者图片')
a_type = models.CharField('附件的类型', max_length=20, blank=True, null=True, choices=type_choices)
class Meta:
db_table = 't_attachment'
verbose_name = '附件表'
verbose_name_plural = verbose_name
ordering = ['id']
def __str__(self):
return self.a_file.name
# 货品库存 模型类
class GoodsInventoryModel(models.Model):
# 期初库存数量在创建好之后不能修改
init_inventory = models.DecimalField('期初库存数量', max_digits=10, decimal_places=2, default=0)
cur_inventory = models.DecimalField('现在库存数量', max_digits=10, decimal_places=2, default=0)
lowest_inventory = models.DecimalField('最低安全库存, 0表示不设置', max_digits=10, decimal_places=2, default=0)
highest_inventory = models.DecimalField('最高安全库存,0表示不设置', max_digits=10, decimal_places=2, default=0)
goods = models.ForeignKey('GoodsModel', related_name='inventory_list', on_delete=models.CASCADE, blank=True,
null=True)
warehouse = models.ForeignKey('basic_info.WarehouseModel', on_delete=models.CASCADE)
# 冗余字段,主要目的:减少联表查询的次数
warehouse_name = models.CharField('仓库的名称', max_length=50)
class Meta:
db_table = 't_goods_inventory'
verbose_name = '货品库存表'
verbose_name_plural = verbose_name
ordering = ['id']
def __str__(self):
return self.warehouse_name + str(self.id)
# 货品(商品)模型类
class GoodsModel(BaseModel):
name = models.CharField(max_length=20, verbose_name='货品名称', unique=True)
specification = models.CharField('规格', max_length=50, null=True, blank=True)
model_number = models.CharField('型号', max_length=50, null=True, blank=True)
color = models.CharField('颜色', max_length=50, null=True, blank=True)
basic_weight = models.CharField('基础重量', max_length=50, null=True, blank=True)
expiration_day = models.IntegerField('保质期', null=True, blank=True)
remark = models.CharField('备注', max_length=512, blank=True, null=True)
number_code = models.CharField('编号或者批号', max_length=28, unique=True)
purchase_price = models.DecimalField('采购价', max_digits=10, decimal_places=2, blank=True, default=0) # 精确到小数点后两位
retail_price = models.DecimalField('零售价', max_digits=10, decimal_places=2, blank=True, default=0) # 精确到小数点后两位
sales_price = models.DecimalField('销售价', max_digits=10, decimal_places=2, blank=True, default=0) # 精确到小数点后两位
lowest_price = models.DecimalField('最低售价', max_digits=10, decimal_places=2, blank=True, default=0) # 精确到小数点后两位
order_number = models.IntegerField('排序号码', default=100)
units = models.ForeignKey('UnitsModel', on_delete=models.SET_NULL, null=True, blank=True)
category = models.ForeignKey('GoodsCategoryModel', on_delete=models.SET_NULL, null=True, blank=True)
images_list = models.CharField('商品附件所对应的id列表', max_length=20, null=True, blank=True) # 字段的值为: 1,2,3,4
class Meta:
db_table = 't_goods'
verbose_name = '货品表'
verbose_name_plural = verbose_name
ordering = ['order_number', 'id']
def __str__(self):
return self.name

@ -0,0 +1,21 @@
from goods_info.models import AttachmentModel
from rest_framework import serializers
class AttachmentsSerializer(serializers.ModelSerializer):
"""
附件和图片的序列化器
"""
# source 如果是字段,会显示字段,如果是方法,会执行方法,不用加括号
# 附件或者图片的文件名
file_name = serializers.CharField(source='a_file.name', read_only=True)
# 附件或者图片的访问地址
file_url = serializers.CharField(source='a_file.url', read_only=True)
# 附件的文件类型(中文)
type_display = serializers.CharField(source='get_a_type_display', read_only=True)
class Meta:
model = AttachmentModel
fields = ['id', 'a_file', 'a_type', 'file_url', 'file_name', 'type_display']

@ -0,0 +1,19 @@
from goods_info.models import GoodsCategoryModel
from rest_framework import serializers
class CategorySerializer(serializers.ModelSerializer):
"""
商品类别的序列化器
"""
# 树形展示
children = serializers.SerializerMethodField(read_only=True)
class Meta:
model = GoodsCategoryModel
fields = ['id', 'number_code', 'remark', 'name', 'order_number', 'parent', 'children']
def get_children(self, obj):
if obj.children:
return CategorySerializer(obj.children, many=True).data
return None

@ -0,0 +1,88 @@
from django.db import transaction
from rest_framework import serializers
from ERP_5.utils.get_inventory import get_inventory_by_goods
from goods_info.models import GoodsModel, GoodsInventoryModel, AttachmentModel
from goods_info.serializer.attachment_serializer import AttachmentsSerializer
from goods_info.serializer.category_serializer import CategorySerializer
from goods_info.serializer.units_serializer import UnitsSerializer
class GoodsInventorySerializer(serializers.ModelSerializer):
"""
货品库存信息的序列化器
"""
class Meta:
model = GoodsInventoryModel
fields = '__all__'
class GoodsBaseSerializer(serializers.ModelSerializer):
"""
商品货品的序列化器
"""
# 商品对应的多个库存记录
inventory_list = GoodsInventorySerializer(many=True, required=True)
class Meta:
model = GoodsModel
fields = '__all__'
def create(self, validated_data):
# 添加货品, 把库存信息先拿出来
inventory_list = validated_data.pop('inventory_list')
with transaction.atomic():
goods = GoodsModel.objects.create(**validated_data)
for item in inventory_list: # item是字典
# item代表一条库存信息, # 新建时,当前库存=初始库存
item['cur_inventory'] = item.get('init_inventory', 0)
GoodsInventoryModel.objects.create(goods=goods, **item)
return goods
def update(self, instance, validated_data):
item_list = validated_data.pop('inventory_list')
for item in item_list: # 遍历每条库存信息,并修改最高库存和最低库存
GoodsInventoryModel.objects.filter(warehouse_name=item['warehouse_name'], goods_id=instance.id).update(
lowest_inventory=item.get('lowest_inventory', 0), highest_inventory=item.get('highest_inventory', 0))
goods = super(GoodsBaseSerializer, self).update(instance=instance, validated_data=validated_data)
return goods
class GoodsGetSerializer(serializers.ModelSerializer):
"""
商品查询的序列化器
目标
1把商品的单位所有信息展示出来
2展示商品信息所属的商品类别所有信息展示出来
3展示商品的所有附件图片所有图片信息展示出来
4展示商品的所有仓库中的库存信息总库存
"""
units = UnitsSerializer(read_only=True)
category = CategorySerializer(read_only=True)
images_list = serializers.SerializerMethodField(read_only=True)
# 当前货品的总库存
cur_inventory = serializers.SerializerMethodField(read_only=True)
class Meta:
model = GoodsModel
fields = '__all__'
def get_images_list(self, obj):
result = [] # 返回的列表, 列表里面是字典(每个字典就是一个附件)
if obj.images_list:
ids = obj.images_list.split(',') # 按照逗号分割
if ids:
for _id in ids:
item = AttachmentModel.objects.get(id=_id)
ser = AttachmentsSerializer(instance=item)
result.append(ser.data)
return result
def get_cur_inventory(self, obj):
# 获取一个商品的总库存
return get_inventory_by_goods(obj.id)

@ -0,0 +1,17 @@
from goods_info.models import UnitsModel
from rest_framework import serializers
class UnitsSerializer(serializers.ModelSerializer):
"""
计量单位的序列化器
"""
# 指定新增的属性,是从一个函数中得到值
units_name = serializers.SerializerMethodField(read_only=True)
class Meta:
model = UnitsModel
fields = ['id', 'basic_name', 'backup_name', 'delete_flag', 'units_name']
def get_units_name(self, obj):
return str(obj) # 就是调用该对象的__str__函数

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,34 @@
"""ERP_5 URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, re_path
from rest_framework.routers import DefaultRouter
from .views import units_views, attachments_views, goods_category_views, goods_views
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
# path('admin/', admin.site.urls),
]
router = DefaultRouter()
router.register('category', goods_category_views.CategoryView)
router.register('units', units_views.UnitsView)
router.register('attachments', attachments_views.AttachmentView)
router.register('goods', goods_views.GoodsInfoViewSet)
urlpatterns += router.urls

@ -0,0 +1,37 @@
import logging
from rest_framework import mixins
from rest_framework import viewsets
from goods_info.models import AttachmentModel
from goods_info.serializer.attachment_serializer import AttachmentsSerializer
logger = logging.getLogger('my')
class AttachmentView(mixins.CreateModelMixin, mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
"""
create: a_file必须是选择的一个文件 a_type:是一个字符串参考模型类代码
附件或者图片--新增
附件或者图片新增, status: 201(成功), return: 新增附件或者图片信息
destroy:
附件或者图片--删除
附件或者图片删除, status: 204(成功), return: None
list:
附件或者图片--获取分页列表
附件或者图片列表信息, status: 200(成功), return: 附件或者图片信息列表
retrieve:
查询某一个附件或者图片
查询指定ID的附件或者图片, status: 200(成功), return: 用户附件或者图片
"""
queryset = AttachmentModel.objects.all()
serializer_class = AttachmentsSerializer

@ -0,0 +1,73 @@
import logging
from django.db.models import Q
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import viewsets
from goods_info.models import GoodsCategoryModel
from goods_info.serializer.category_serializer import CategorySerializer
from ERP_5.utils.base_views import MultipleDestroyMixin
logger = logging.getLogger('my')
class CategoryView(viewsets.ModelViewSet, MultipleDestroyMixin):
"""
create:
货品类别--新增
货品类别新增, status: 201(成功), return: 新增货品类别信息
destroy:
货品类别--删除
货品类别删除, status: 204(成功), return: None
multiple_delete:
货品类别--批量删除,必传参数ids=[1,2,3,4...]
货品类别批量删除, status: 204(成功), return: None
update:
货品类别--修改,
货品类别修改, status: 200(成功), return: 修改后的货品类别信息
partial_update:
货品类别--局部修改,可以传参任意属性的值服务器会修改指定的属性值
货品类别局部修改, status: 200(成功), return: 修改后的货品类别信息
list:
货品类别--获取类别列表可选get请求参数:pid如果没有传则查询顶级类别否则查询指定pid下的子类别
货品类别列表信息, status: 200(成功), return: 货品类别信息列表
retrieve:
查询某一个货品类别
查询指定ID的货品类别, status: 200(成功), return: 用户货品类别
"""
queryset = GoodsCategoryModel.objects.all()
serializer_class = CategorySerializer
def get_queryset(self):
if self.action == 'list': # 判断是否查询顶级列表还是某个子列表
# 获取请求参数(在get中)pid
pid = self.request.query_params.get('pid', None)
if pid:
return GoodsCategoryModel.objects.filter(parent__id=pid).all()
return GoodsCategoryModel.objects.filter(parent__isnull=True).all()
else:
return GoodsCategoryModel.objects.all()
# 分页参数必须是query_param(看源码)
pid_param = openapi.Parameter(name='pid', in_=openapi.IN_QUERY, description="父级的ID可以不传", type=openapi.TYPE_INTEGER)
@swagger_auto_schema(manual_parameters=[pid_param],
operation_description="商品类别的列表")
def list(self, request, *args, **kwargs):
return super(CategoryView, self).list(request=request, *args, **kwargs)

@ -0,0 +1,121 @@
from django.db.models import Q
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.viewsets import ModelViewSet
from ERP_5.utils.base_views import MultipleDestroyMixin, MultipleOpenMixin
from ERP_5.utils.paginations import GlobalPagination
from goods_info.models import GoodsModel
from goods_info.serializer.goods_serializer import GoodsBaseSerializer, GoodsGetSerializer
class GoodsInfoViewSet(ModelViewSet, MultipleDestroyMixin, MultipleOpenMixin):
"""
create:
货品(商品)信息--新增,注意其中images_list="1,2,3,4";里面是附件的ID
货品(商品)信息新增, status: 201(成功), return: 新增货品(商品)信息信息
destroy:
货品(商品)信息--删除
货品(商品)信息删除, status: 204(成功), return: None
multiple_delete:
货品(商品)信息--批量删除,必传参数ids=[1,2,3,4...]
货品(商品)信息批量删除, status: 204(成功), return: None
multiple_open:
货品(商品)信息--批量启用或者禁用,必传(json)参数ids=[1,2,3,4...](列表中可以只有一个)is_open=1/0
{
"ids":[1,2],
"is_open":"0"
}
is_open=1表示禁用is_open=0表示启用
货品(商品)信息批量启用或者禁用, status: 204(成功), return: None
update:
货品(商品)信息--修改,注意其中images_list="1,2,3,4";里面是附件的ID
货品(商品)信息修改, status: 200(成功), return: 修改后的货品(商品)信息信息
partial_update:
货品(商品)信息--局部修改,可以传参任意属性的值服务器会修改指定的属性值
货品(商品)信息局部修改, status: 200(成功), return: 修改后的货品(商品)信息信息
list:
货品(商品)信息--该接口可以弃用
货品(商品)信息列表信息, status: 200(成功), return: 货品(商品)信息信息列表
retrieve:
查询某一个货品(商品)信息
查询指定ID的货品(商品)信息, status: 200(成功), return: 用户货品(商品)信息
"""
queryset = GoodsModel.objects.all()
serializer_class = GoodsBaseSerializer
pagination_class = GlobalPagination
def get_queryset(self):
if self.action == 'find': # 过滤查询
# 获取请求参数(在json中)
keyword = self.request.data.get('keyword', None)
color = self.request.data.get('color', None)
category = self.request.data.get('category', 0)
number_code = self.request.data.get('number_code', None)
basic_weight = self.request.data.get('basic_weight', None)
expiration_day = self.request.data.get('expiration_day', 0)
delete_flag = self.request.data.get('delete_flag', None)
query = Q() # 就是为了完成条件的组合
if keyword:
child_query = Q()
child_query.add(Q(name__contains=keyword), 'OR')
child_query.add(Q(specification=keyword), 'OR')
child_query.add(Q(model_number=keyword), 'OR')
query.add(child_query, 'AND')
if color:
query.add(Q(color=color), 'AND')
if category:
query.add(Q(category__id=category), 'AND')
if number_code:
query.add(Q(number_code__contains=number_code), 'AND')
if basic_weight:
query.add(Q(basic_weight=basic_weight), 'AND')
if expiration_day:
query.add(Q(expiration_day=expiration_day), 'AND')
if delete_flag:
query.add(Q(delete_flag=delete_flag), 'AND')
return GoodsModel.objects.filter(query).all()
else:
return GoodsModel.objects.all()
def get_serializer_class(self):
if self.action == 'find' or self.action == 'retrieve':
return GoodsGetSerializer
else:
return GoodsBaseSerializer
params = openapi.Schema(type=openapi.TYPE_OBJECT, properties={
'keyword': openapi.Schema(type=openapi.TYPE_STRING, description="名称的关键字或者规格和型号"),
'color': openapi.Schema(type=openapi.TYPE_STRING, description="颜色"),
'number_code': openapi.Schema(type=openapi.TYPE_STRING, description="批号(序列号)"),
'basic_weight': openapi.Schema(type=openapi.TYPE_STRING, description="基础质量"),
'delete_flag': openapi.Schema(type=openapi.TYPE_STRING, description="状态0或者1"),
'category': openapi.Schema(type=openapi.TYPE_INTEGER, description="类别的ID"),
'expiration_day': openapi.Schema(type=openapi.TYPE_INTEGER, description="保质期天数"),
})
# 分页参数必须是query_param(看源码)
page_param = openapi.Parameter(name='page', in_=openapi.IN_QUERY, description="页号", type=openapi.TYPE_INTEGER)
size_param = openapi.Parameter(name='size', in_=openapi.IN_QUERY, description="每页显示数量", type=openapi.TYPE_INTEGER)
@swagger_auto_schema(method='POST', request_body=params, manual_parameters=[page_param, size_param],
operation_description="商品搜索过滤")
@action(methods=['POST'], detail=False)
def find(self, request, *args, **kwargs):
return super(GoodsInfoViewSet, self).list(request, *args, **kwargs)

@ -0,0 +1,89 @@
import logging
from django.db.models import Q
from django.utils.decorators import method_decorator
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import viewsets
from goods_info.models import UnitsModel
from goods_info.serializer.units_serializer import UnitsSerializer
from ERP_5.utils.paginations import GlobalPagination
from ERP_5.utils.base_views import MultipleDestroyMixin, MultipleOpenMixin
logger = logging.getLogger('my')
param3 = openapi.Parameter(name='name', type=openapi.TYPE_STRING, description="名称关键字", in_=openapi.IN_QUERY)
@method_decorator(name='list', decorator=swagger_auto_schema(
manual_parameters=[param3],
operation_description="计量单位的搜索过滤")
)
class UnitsView(viewsets.ModelViewSet, MultipleDestroyMixin, MultipleOpenMixin):
"""
create:
计量单位--新增
计量单位新增, status: 201(成功), return: 新增计量单位信息
destroy:
计量单位--删除
计量单位删除, status: 204(成功), return: None
multiple_delete:
计量单位--批量删除,必传参数ids=[1,2,3,4...]
计量单位批量删除, status: 204(成功), return: None
multiple_open:
计量单位--批量启用或者禁用,必传(json)参数ids=[1,2,3,4...](列表中可以只有一个)is_open=1/0
{
"ids":[1,2],
"is_open":"0"
}
is_open=1表示禁用is_open=0表示启用
计量单位批量启用或者禁用, status: 204(成功), return: None
update:
计量单位--修改,
计量单位修改, status: 200(成功), return: 修改后的计量单位信息
partial_update:
计量单位--局部修改,可以传参任意属性的值服务器会修改指定的属性值
计量单位局部修改, status: 200(成功), return: 修改后的计量单位信息
list:
计量单位--获取分页列表可选json参数:name(名称)
{
"name":"长沙",
}
计量单位列表信息, status: 200(成功), return: 计量单位信息列表
retrieve:
查询某一个计量单位
查询指定ID的计量单位, status: 200(成功), return: 用户计量单位
"""
queryset = UnitsModel.objects.all()
serializer_class = UnitsSerializer
pagination_class = GlobalPagination
def get_queryset(self):
if self.action == 'list': # 过滤查询
# 获取请求参数(在json中)namephone, mobile
name = self.request.data.get('name', None)
query = Q()
if name:
# 关键字查询两个字段
query.add(Q(basic_name__contains=name), 'OR') # 多条件组合
query.add(Q(backup_name__contains=name), 'OR')
return UnitsModel.objects.filter(query).all()
else:
return UnitsModel.objects.all()

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PurchaseInfoConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'purchase_info'

@ -0,0 +1,71 @@
from django.db import models
from ERP_5.utils.base_model import BaseModel
# 采购单 模型类
class PurchaseModel(BaseModel):
invoices_date = models.DateTimeField('单据日期')
number_code = models.CharField('单据编号,不让用户填写', max_length=28)
discount = models.DecimalField('优惠率,最多精确到小数点后两位', max_digits=5, decimal_places=2, blank=True, default=0)
discount_money = models.DecimalField('优惠金额(付款优惠),最多精确到小数点后两位', max_digits=10, decimal_places=2, blank=True,
default=0)
remark = models.CharField('备注', max_length=512, blank=True, null=True)
last_amount = models.DecimalField('优惠后总金额,最多精确到小数点后两位', max_digits=13, decimal_places=2, blank=True, default=0)
deposit = models.DecimalField('支付定金,最多精确到小数点后两位', max_digits=10, decimal_places=2, blank=True, default=0)
number_count = models.DecimalField('采购数量,最多精确到小数点后两位', max_digits=10, decimal_places=2, blank=True, default=0)
status = models.CharField('状态,0:未审核,1:已审核,2:部分入库,3:全部入库,4:完成采购,5:已付定金', max_length=1, default='0')
operator_user = models.ForeignKey('erp_system.UserModel', related_name='operator_purchase_list', null=True,
blank=True, on_delete=models.SET_NULL,
verbose_name='采购操作人员,不能修改')
# 增加一个冗余字段
operator_user_name = models.CharField('操作人员的真实姓名', max_length=20, null=True, blank=True)
check_user = models.ForeignKey('erp_system.UserModel', null=True, blank=True, on_delete=models.SET_NULL,
verbose_name='审核人员,不能修改')
# 增加一个冗余字段
check_user_name = models.CharField('操作人员的真实姓名', max_length=20, null=True, blank=True)
account = models.ForeignKey('basic_info.SettlementAccountModel', null=True, blank=True, on_delete=models.SET_NULL,
verbose_name='结算账户,审核之后不能改')
supplier = models.ForeignKey('basic_info.SupplierModel', null=True, blank=True, on_delete=models.SET_NULL,
verbose_name='供应商,审核之后不能改')
# 增加一个冗余字段
supplier_name = models.CharField('供应商名称', max_length=30, null=True, blank=True)
attachment_list = models.CharField('附件的id列表字段的值为: 1,2,3,4', max_length=20, null=True, blank=True)
class Meta:
db_table = 't_purchase'
verbose_name = '采购表'
verbose_name_plural = verbose_name
ordering = ['id']
# 采购单中的货品(采购项) 模型类
class PurchaseItemModel(BaseModel):
# 个个也都是冗余字段
name = models.CharField(max_length=20, verbose_name='货品名称')
specification = models.CharField('货品规格', max_length=50, null=True, blank=True)
number_code = models.CharField('货品的编号或者批号', max_length=28, null=True, blank=True)
remark = models.CharField('备注', max_length=512, blank=True, null=True)
purchase_count = models.DecimalField('采购数量,最多精确到小数点后两位', max_digits=10, decimal_places=2, blank=True, default=0)
purchase_price = models.DecimalField('采购单价,最多精确到小数点后两位', max_digits=10, decimal_places=2, blank=True, default=0)
purchase_money = models.DecimalField('采购金额,最多精确到小数点后两位', max_digits=10, decimal_places=2, blank=True, default=0)
purchase = models.ForeignKey('PurchaseModel', related_name='item_list', null=True, blank=True,
on_delete=models.CASCADE,
verbose_name='采购单')
goods = models.ForeignKey('goods_info.GoodsModel', null=True, blank=True, on_delete=models.SET_NULL,
verbose_name='货品')
class Meta:
db_table = 't_purchase_items'
verbose_name = '采购项表'
verbose_name_plural = verbose_name
ordering = ['id']
def __str__(self):
return self.name + ' ' + self.specification

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,31 @@
"""ERP_5 URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, re_path
from rest_framework.routers import DefaultRouter
# from .views import units_views, attachments_views, goods_category_views, goods_views
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
# path('admin/', admin.site.urls),
]
router = DefaultRouter()
# router.register('category', goods_category_views.CategoryView)
urlpatterns += router.urls

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

@ -0,0 +1,16 @@
"""
ASGI config for ERP_5 project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ERP_5.settings')
application = get_asgi_application()

@ -0,0 +1,15 @@
import os
from celery import Celery
from django.conf import settings
import django
# 设置环境变量
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ERP_5.settings.dev')
# 注册Celery的APP, ERP_5必须是项目名字
celery_app = Celery('ERP_5')
# 绑定配置文件
celery_app.config_from_object('django.conf:settings', namespace='CELERY')
# 自动从settings.INSTALLED_APPS注册的应用中加载tasks.py
celery_app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

@ -0,0 +1,279 @@
"""
Django settings for ERP_5 project.
Generated by 'django-admin startproject' using Django 3.2.16.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
import sys
import os
import datetime
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-o!nl319=c4gr)@c)n#@*kq)zil-i(4puha&59&1o*wd0cbrj!5'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'corsheaders',
'erp_system',
'basic_info',
'goods_info',
'purchase_info',
# 'django.contrib.staticfiles', # required for serving swagger ui's css/js files
'drf_yasg',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
CORS_ORIGIN_WHITELIST = (
'http://127.0.0.1:8080',
'http://localhost:8080',
)
CORS_ALLOW_CREDENTIALS = True
ROOT_URLCONF = 'ERP_5.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates']
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'ERP_5.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'erp_5',
'USER': 'root',
'PASSWORD': '123123',
'HOST': '127.0.0.1',
'PORT': '3306',
}
}
# 配置Redis数据库
CACHES = {
"default": { # 默认
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'zh-Hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/'
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': { #
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(filename)s: %(lineno)d %(message)s'
},
'simple': {
'format': '%(levelname)s %(module)s %(lineno)d %(message)s'
},
},
'filters': { #
'require_debug_true': { #
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': { #
'console': { #
'level': 'INFO',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'my_con': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
},
'file': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(BASE_DIR, 'logs/mangguo.log'),
'maxBytes': 300 * 1024 * 1024,
'backupCount': 10,
'formatter': 'verbose'
},
},
'loggers': {
'my': {
# 'handlers': ['console', 'file'],
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True,
},
'django.db.backends': {
# 'handlers': ['console', 'file'],
'handlers': ['my_con'],
'propagate': True,
'level': 'DEBUG',
},
}
}
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
'DEFAULT_PERMISSION_CLASSES':
(
# 'rest_framework.permissions.IsAuthenticated',
# 'ERP_5.utils.rbac_permissions.RbacPermission', # 自定义的权限验证
)
}
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
'JWT_RESPONSE_PAYLOAD_HANDLER': 'ERP_5.utils.jwt_handler.my_jwt_response_msg',
}
# 添加自定义用户模型类(应用名.模型类名)
AUTH_USER_MODEL = 'erp_system.UserModel'
# 指定自定义认证类路径
AUTHENTICATION_BACKENDS = ['erp_system.erp_auth.UserLoginAuth']
# SWAGGER 接口文档支持JWT的自动认证自动在请求头中加入token
SWAGGER_SETTINGS = {
# 'PERSIST_AUTH': True,
'REFETCH_SCHEMA_WITH_AUTH': True,
'REFETCH_SCHEMA_ON_LOGOUT': True,
'SECURITY_DEFINITIONS': {
'JWT': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header'
},
}
}
# celery
# Broker配置使用Redis作为消息中间件
CELERY_BROKER_URL = 'redis://127.0.0.1:6379/4'
# BACKEND配置这里使用redis
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/5'
# 结果序列化方案
CELERY_RESULT_SERIALIZER = 'json'
# 任务结果过期时间,秒
CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24
# 时区配置
CELERY_TIMEZONE = 'Asia/Shanghai'
BASE_API = 'api/' # 项目BASE API, 如设置时必须以/结尾
# 权限认证白名单
WHITE_LIST = [f'/{BASE_API}users/login/', f'/{BASE_API}users/register/', f'/docs/.*', f'/swagger/.*']
REGEX_URL = '^{url}$' # 权限匹配时,严格正则url
# 设置媒体路由地址信息
MEDIA_URL = '/media/'
# # 配置上传件存放的目录获取media文件夹的完整路径信息
MEDIA_ROOT = BASE_DIR / 'media'

@ -0,0 +1,126 @@
"""
Django settings for ERP_5 project.
Generated by 'django-admin startproject' using Django 3.2.16.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-o!nl319=c4gr)@c)n#@*kq)zil-i(4puha&59&1o*wd0cbrj!5'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'ERP_5.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates']
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'ERP_5.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/'
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

@ -0,0 +1,51 @@
"""ERP_5 URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.contrib import admin
from django.urls import path, include, re_path
from django.views.static import serve
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions
from rest_framework.documentation import include_docs_urls
from ERP_5.utils.base_views import GenerateCode
schema_view = get_schema_view(
openapi.Info(
title="接口 API文档",
default_version='v1',
description="马士兵ERP系统项目接口文档",
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('erp_system.urls')),
path('api/', include('basic_info.urls')),
path('api/', include('goods_info.urls')),
path('api/', include('purchase_info.urls')),
re_path(r'^api/generate_code/$', GenerateCode.as_view()),
# 媒体资源文件的访问路由
re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
path('docs/', include_docs_urls(title='ERP系统的接口文档')),
# swagger接口文档的路由
path('swagger<format>/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]

@ -0,0 +1,11 @@
from django.db import models
class BaseModel(models.Model):
"""所有模型的父类"""
delete_flag = models.CharField('是否启用', max_length=1, default='0', help_text='是否启用')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间', help_text='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间', help_text='更新时间')
class Meta:
abstract = True

@ -0,0 +1,85 @@
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status, views
from rest_framework.decorators import action
from rest_framework.response import Response
from ERP_5.utils.cont import NumberPrefix
from ERP_5.utils.generate_code import generate_code
class MultipleDestroyMixin:
"""
自定义批量删除的视图函数
"""
del_ids = openapi.Schema(type=openapi.TYPE_OBJECT, required=['ids'], properties={
'ids': openapi.Schema(type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_INTEGER),
description="选择哪些需要删除的ID主键列表")
})
@swagger_auto_schema(method='delete', request_body=del_ids, operation_description="批量删除")
@action(methods=['delete'], detail=False)
def multiple_delete(self, request, *args, **kwargs):
delete_ids = request.data.get('ids')
if not delete_ids:
return Response(data={'detail': '参数错误,ids为必传参数'}, status=status.HTTP_400_BAD_REQUEST)
if not isinstance(delete_ids, list):
return Response(data={'detail': 'ids格式错误,必须为List'}, status=status.HTTP_400_BAD_REQUEST)
queryset = self.get_queryset()
del_queryset = queryset.filter(id__in=delete_ids)
if len(delete_ids) != del_queryset.count():
return Response(data={'detail': '删除数据不存在'}, status=status.HTTP_400_BAD_REQUEST)
del_queryset.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class MultipleOpenMixin:
"""
自定义批量启用或者禁用的视图函数
"""
del_ids = openapi.Schema(type=openapi.TYPE_OBJECT, required=['ids'], properties={
'ids': openapi.Schema(type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_INTEGER),
description="选择哪些批量处理的ID主键列表"),
'is_open': openapi.Schema(type=openapi.TYPE_STRING, description="是否启用启用0禁用1")
})
@swagger_auto_schema(method='delete', request_body=del_ids, operation_description="批量启用或者禁用")
@action(methods=['delete'], detail=False)
def multiple_open(self, request, *args, **kwargs):
delete_ids = request.data.get('ids')
is_open = request.data.get('is_open') # 是否启用启用表示0禁用表示:1。
if not delete_ids:
return Response(data={'detail': '参数错误,ids为必传参数'}, status=status.HTTP_400_BAD_REQUEST)
if not isinstance(delete_ids, list):
return Response(data={'detail': 'ids格式错误,必须为List'}, status=status.HTTP_400_BAD_REQUEST)
queryset = self.get_queryset()
del_queryset = queryset.filter(id__in=delete_ids)
if len(delete_ids) != del_queryset.count():
return Response(data={'detail': '传入数据不存在'}, status=status.HTTP_400_BAD_REQUEST)
del_queryset.update(delete_flag=is_open)
return Response(status=status.HTTP_204_NO_CONTENT)
# 生成各种编号的接口
class GenerateCode(views.APIView):
query_param = openapi.Parameter(name='prefix', in_=openapi.IN_QUERY,
description="批号的前缀参考cont.py", type=openapi.TYPE_STRING)
@swagger_auto_schema(manual_parameters=[query_param])
def get(self, request, *args, **kwargs):
"""
生成各种编号的接口必须传一个前缀: /api/generate_code/prefix=ord可以参考cont.py
返回一个28位的编号字符串, return code就是生成的编号
"""
prefix = request.query_params.get('prefix', None)
if prefix:
if prefix in NumberPrefix.__members__:
code = generate_code(NumberPrefix[prefix].value)
return Response(data={'code': code}, status=status.HTTP_200_OK)
else:
return Response(data={'detail': 'prefix没有配置参考cont.py'}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(data={'detail': 'prefix没有该参数必须传'}, status=status.HTTP_400_BAD_REQUEST)

@ -0,0 +1,47 @@
import json
from django_redis import get_redis_connection
from erp_system.models import PermissionsModel
def cache_permissions_by_user(user):
"""
把当前用户的权限信息缓存到Redis数据库中
Redis 中存放用户权限的结构为 user_用户ID---> 字典{key:path, value[列表] }
"""
permission_ids = user.roles.values_list('permissions', flat=True).distinct()
# permission_list 返回是列表和字典的嵌套,如下:[{'id':1, 'path': '/xxx/xxx'}, {'id':2, 'path': '/xxx/aaa'}]
permission_list = PermissionsModel.objects.filter(id__in=permission_ids, is_menu=False).values('id', 'path', 'method', 'name')
if not permission_list.exists():
# 该用户根本就没有分配权限, 直接返回
return None
permission_dict = {} # 存放权限的字典
for permission in permission_list: # permission是字典
# 去除不可见字符, \u200b是Unicode中的零宽度字符可以理解为不可见字符。
method = str(permission.get('method')).replace('\u200b', '')
path = str(permission.get('path')).replace('\u200b', '')
_name = str(permission.get('name')).replace('\u200b', '')
_id = permission.get('id')
if permission_dict.get(path):
# 如果字典中已经存在一个相同path的权限把新权限追加
permission_dict[path].append({
'method': method,
'sign': _name,
'id': _id
})
else:
permission_dict[path] = [{
'method': method,
'sign': _name,
'id': _id
}]
# 因为redis中的值只能存放字节 由字符串转换字节 。 json格式的字符串
for key in permission_dict:
permission_dict[key] = json.dumps(permission_dict[key])
redis_conn = get_redis_connection('default')
redis_conn.hmset(f'user_{user.id}', permission_dict)

@ -0,0 +1,17 @@
from enum import Enum, unique
"""
各个不同类型的前缀枚举类
"""
@unique
class NumberPrefix(Enum): # 定义编号的前缀
# 结算账户编号的前缀
acc = 'ACC'
# 销售订单编号的前缀
ord = 'ORD'
# 商品类别编号的前缀
cat = 'CAT'
# 商品信息编号的前缀
goo = 'GOO'

@ -0,0 +1,22 @@
import string
import time
import random
def generate_code(prefix):
"""
生成28位流水编号: 3位前缀 + 14位的时间 + 7位的微秒 + 4位随机数
"""
seeds = string.digits
random_str = random.choices(seeds, k=4)
random_str = "".join(random_str)
code_no = "%s%s%s%s" % (prefix,
time.strftime('%Y%m%d%H%M%S', time.localtime(time.time())),
str(time.time()).replace('.', '')[-7:],
random_str)
return code_no
if __name__ == '__main__':
print(generate_code('ORD'))

@ -0,0 +1,22 @@
from django.db.models import Sum
from goods_info.models import GoodsInventoryModel
def get_inventory_by_goods(goods_id, warehouse_id=0):
"""
1可以返回指定货品的总库存
2可以返回知道货品指定仓库的库存
:param goods_id: 指定的货品ID
:param warehouse_id: 指定的仓库ID如果为0则查询所有仓库
:return:
"""
sum_inventory = 0
if warehouse_id == 0: # 查询总库存
result = GoodsInventoryModel.objects.filter(goods_id=goods_id).aggregate(sum=Sum('cur_inventory'))
else:
result = GoodsInventoryModel.objects.filter(goods_id=goods_id, warehouse_id=warehouse_id).aggregate(sum=Sum('cur_inventory'))
if result:
sum_inventory = result['sum']
return sum_inventory

@ -0,0 +1,16 @@
def my_jwt_response_msg(token, user=None, request=None):
"""
自定义JWT响应的数据格式默认只有token
:param token:
:param user:
:param request:
:return:
"""
return {
'token': token,
'user_id': user.id,
'user_name': user.username,
}

@ -0,0 +1,10 @@
from rest_framework.pagination import PageNumberPagination
class GlobalPagination(PageNumberPagination):
"""
list/?page=4&size=20
"""
page_size = 10 # 默认每页数目
page_size_query_param = 'size' # 前端发送的每页数目关键字名默认为None
max_page_size = 100 # 前端最多能设置的每页数量

@ -0,0 +1,70 @@
import json
import re
from django.conf import settings
from django_redis import get_redis_connection
from rest_framework.permissions import BasePermission
import logging
from ERP_5.utils.cache_permissions import cache_permissions_by_user
logger = logging.getLogger('my')
class RbacPermission(BasePermission):
"""
自定义权限认证
"""
def do_url(self, url):
"""
得到完整的访问URL
"""
base_api = settings.BASE_API
uri = '/' + base_api + '/' + url + '/'
return re.sub('/+', '/', uri) # 防止访问地址中出现重复的/,比如://users/hello/
def has_permission(self, request, view):
"""
判断是否有权限该函数必须要重写
思路和步骤
1获取请求的URL和请求方法methoduser用户对象
2判断是否白名单的url, 或者用户的角色是admin也直接放行
3从redis中得到当前登录用户的所有权限
4判断是否存在权限
"""
request_url = request.path # api/users/login/ == api/users/login/
request_method = request.method
logger.info(f'请求地址:{request_url}, 请求方法: {request_method}')
for safe_url in settings.WHITE_LIST:
if re.match(settings.REGEX_URL.format(url=safe_url), request_url):
# 白名单的url直接返回
return True
user = request.user
role_name_list = user.roles.values_list('name', flat=True)
logger.info(role_name_list)
if 'admin' in role_name_list:
return True
redis_conn = get_redis_connection('default') # 获得默认redis库的连接
if not redis_conn.exists(f'user_{user.id}'): # redis中没有缓存权限数据
cache_permissions_by_user(user)
# 得到所有的权限中key其中key是请求url地址
url_keys = redis_conn.hkeys(f'user_{user.id}')
for url_key in url_keys:
# 查看在redis中是否存在该接口对于的权限
if re.match(settings.REGEX_URL.format(url=self.do_url(url_key.decode())), request_url):
redis_key = url_key.decode() # redis中是字节数据
break
else: # 这行代码可以不加,如果严格规定权限。
return False
permissions = json.loads(redis_conn.hget(f'user_{user.id}', redis_key).decode())
method_hit = False # 同一接口配置不同权限验证
for permission in permissions:
if permission.get('method') == request_method:
method_hit = True
break
return method_hit

@ -0,0 +1,20 @@
from celery import Task
class HookTask(Task):
"""
任务钩子 监控任务的执行
"""
# 成功时执行
def on_success(self, ret_val, task_id, args, kwargs):
print('ret_val', ret_val)
print(f'task id:{task_id} , arg:{args} , successful !')
# 失败时执行
def on_failure(self, exc, task_id, args, kwargs, error_info):
print(f'task id:{task_id} , arg:{args} , failed ! errors: {exc}')
# 任务重试时执行
def on_retry(self, exc, task_id, args, kwargs, error_info):
print(f'task id:{task_id} , arg:{args} , retry ! errors: {exc}')

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save