forked from erp-dev/erp
feat: added user profile model & api & admin, it uses by found user with merchant
This commit is contained in:
300
api_man/employee_user_binding.md
Normal file
300
api_man/employee_user_binding.md
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
# Employee-User Binding API Documentation
|
||||||
|
|
||||||
|
This document explains how to use the employee API endpoints to bind a Django User object to an Employee object, either during creation or update operations. It also includes information about the UserProfile model which extends the Django User model with merchant association.
|
||||||
|
|
||||||
|
## 1. Model Relationship Overview
|
||||||
|
|
||||||
|
### 1.1 Core Model Relationships
|
||||||
|
The system has three key models related to user management:
|
||||||
|
- `django.contrib.auth.models.User`: Django's built-in user model for authentication
|
||||||
|
- `Employee`: Represents an employee in the system
|
||||||
|
- `UserProfile`: Extends the User model with additional information (merchant association)
|
||||||
|
|
||||||
|
### 1.2 Field Relationships
|
||||||
|
- `Employee` has a **one-to-one** field `sys_user` that references `django.contrib.auth.models.User`
|
||||||
|
- Field name: `sys_user`
|
||||||
|
- It accepts the User ID as input
|
||||||
|
- It's a nullable field, so an Employee can exist without being bound to a User initially
|
||||||
|
|
||||||
|
- `UserProfile` has a **one-to-one** field `user` that references `django.contrib.auth.models.User`
|
||||||
|
- Field name: `user`
|
||||||
|
- It also has a `merchant` field that links the user to a specific merchant
|
||||||
|
- A User can have only one UserProfile, and a UserProfile belongs to only one merchant
|
||||||
|
|
||||||
|
### 1.3 Relationship Diagram
|
||||||
|
```
|
||||||
|
+------------------------+ +------------------------+
|
||||||
|
| django.contrib.auth.User| | Employee |
|
||||||
|
+------------------------+ +------------------------+
|
||||||
|
| id | | id |
|
||||||
|
| username |<----->| sys_user | (one-to-one)
|
||||||
|
| email | | name |
|
||||||
|
| password | | position |
|
||||||
|
+------------------------+ +------------------------+
|
||||||
|
|
||||||
|
+------------------------+ +------------------------+
|
||||||
|
| django.contrib.auth.User| | UserProfile |
|
||||||
|
+------------------------+ +------------------------+
|
||||||
|
| id |<----->| user | (one-to-one)
|
||||||
|
| username | | merchant |
|
||||||
|
| email | | description |
|
||||||
|
| password | +------------------------+
|
||||||
|
+------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. API Endpoints
|
||||||
|
|
||||||
|
### 2.1 Employee API Endpoints
|
||||||
|
Base URL: `/api_man/employees/`
|
||||||
|
|
||||||
|
| Method | URL Pattern | Action |
|
||||||
|
|--------|-------------|--------|
|
||||||
|
| GET | `/api_man/employees/` | List all employees |
|
||||||
|
| GET | `/api_man/employees/{id}/` | Retrieve a single employee |
|
||||||
|
| POST | `/api_man/employees/` | Create a new employee |
|
||||||
|
| PUT | `/api_man/employees/{id}/` | Update an existing employee |
|
||||||
|
| PATCH | `/api_man/employees/{id}/` | Partially update an existing employee |
|
||||||
|
| DELETE | `/api_man/employees/{id}/` | Delete an employee |
|
||||||
|
|
||||||
|
### 2.2 UserProfile API Endpoints
|
||||||
|
Base URL: `/api_man/user-profiles/`
|
||||||
|
|
||||||
|
| Method | URL Pattern | Action |
|
||||||
|
|--------|-------------|--------|
|
||||||
|
| GET | `/api_man/user-profiles/` | List all user profiles |
|
||||||
|
| GET | `/api_man/user-profiles/{id}/` | Retrieve a single user profile |
|
||||||
|
| POST | `/api_man/user-profiles/` | Create a new user profile |
|
||||||
|
| PUT | `/api_man/user-profiles/{id}/` | Update an existing user profile |
|
||||||
|
| PATCH | `/api_man/user-profiles/{id}/` | Partially update an existing user profile |
|
||||||
|
| DELETE | `/api_man/user-profiles/{id}/` | Delete a user profile |
|
||||||
|
|
||||||
|
## 3. Binding User to Employee
|
||||||
|
|
||||||
|
### 3.1 During Employee Creation
|
||||||
|
When creating a new Employee, you can directly include the `sys_user` field with the User ID to bind them immediately.
|
||||||
|
|
||||||
|
#### Request Body Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "张三",
|
||||||
|
"position": 1, // EmployeeType ID
|
||||||
|
"mobile": "13800138000",
|
||||||
|
"status": "在职",
|
||||||
|
"sys_user": 123 // User ID to bind
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 456,
|
||||||
|
"name": "张三",
|
||||||
|
"position": { "id": 1, "name": "仓库管理员" },
|
||||||
|
"mobile": "13800138000",
|
||||||
|
"status": "在职",
|
||||||
|
"sys_user": 123, // User ID is now bound
|
||||||
|
// Other fields...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 During Employee Update
|
||||||
|
To bind an existing User to an existing Employee, use the PUT or PATCH method and include the `sys_user` field.
|
||||||
|
|
||||||
|
#### PUT Request Body Example (Full Update):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "张三",
|
||||||
|
"position": 1,
|
||||||
|
"mobile": "13800138000",
|
||||||
|
"status": "在职",
|
||||||
|
"sys_user": 123 // New User ID to bind
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PATCH Request Body Example (Partial Update):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sys_user": 123 // User ID to bind
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 456,
|
||||||
|
"name": "张三",
|
||||||
|
"position": { "id": 1, "name": "仓库管理员" },
|
||||||
|
"mobile": "13800138000",
|
||||||
|
"status": "在职",
|
||||||
|
"sys_user": 123, // User ID is now bound
|
||||||
|
// Other fields...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Unbinding User from Employee
|
||||||
|
To unbind a User from an Employee, set the `sys_user` field to `null` using PUT or PATCH.
|
||||||
|
|
||||||
|
#### PATCH Request Body Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sys_user": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 456,
|
||||||
|
"name": "张三",
|
||||||
|
"position": { "id": 1, "name": "仓库管理员" },
|
||||||
|
"mobile": "13800138000",
|
||||||
|
"status": "在职",
|
||||||
|
"sys_user": null, // User is now unbound
|
||||||
|
// Other fields...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. UserProfile Management
|
||||||
|
|
||||||
|
### 4.1 Creating a UserProfile
|
||||||
|
When creating a new UserProfile, you need to provide the User ID and Merchant ID.
|
||||||
|
|
||||||
|
#### Request Body Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user": 123, // User ID
|
||||||
|
"merchant": 1, // Merchant ID
|
||||||
|
"description": "User profile for system access"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 789,
|
||||||
|
"user": {
|
||||||
|
"id": 123,
|
||||||
|
"username": "system_user",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"is_active": true,
|
||||||
|
"is_superuser": false,
|
||||||
|
"last_login": "2025-11-24T12:34:56Z"
|
||||||
|
},
|
||||||
|
"merchant": 1,
|
||||||
|
"description": "User profile for system access",
|
||||||
|
"created_at": "2025-11-24T13:00:00Z",
|
||||||
|
"updated_at": "2025-11-24T13:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Updating a UserProfile
|
||||||
|
To update an existing UserProfile, use the PUT or PATCH method.
|
||||||
|
|
||||||
|
#### PATCH Request Body Example (Partial Update):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"description": "Updated user profile for system access"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 789,
|
||||||
|
"user": {
|
||||||
|
"id": 123,
|
||||||
|
"username": "system_user",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"is_active": true,
|
||||||
|
"is_superuser": false,
|
||||||
|
"last_login": "2025-11-24T12:34:56Z"
|
||||||
|
},
|
||||||
|
"merchant": 1,
|
||||||
|
"description": "Updated user profile for system access",
|
||||||
|
"created_at": "2025-11-24T13:00:00Z",
|
||||||
|
"updated_at": "2025-11-24T13:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Validation Rules
|
||||||
|
|
||||||
|
### 5.1 Employee-User Binding Rules
|
||||||
|
1. **One-to-one constraint**: A User can only be bound to one Employee at a time.
|
||||||
|
2. **Permission check**: Only super users or users with appropriate permissions can perform binding operations.
|
||||||
|
3. **User existence**: The provided User ID must exist in the system.
|
||||||
|
4. **Merchant isolation**: Both the User (via UserProfile) and Employee must belong to the same Merchant.
|
||||||
|
|
||||||
|
### 5.2 UserProfile Rules
|
||||||
|
1. **One-to-one constraint**: A User can only have one UserProfile at a time.
|
||||||
|
2. **Permission check**: Only users with appropriate permissions can access UserProfiles from the same merchant.
|
||||||
|
3. **User existence**: The provided User ID must exist in the system.
|
||||||
|
4. **Merchant existence**: The provided Merchant ID must exist in the system.
|
||||||
|
|
||||||
|
## 6. Error Handling
|
||||||
|
|
||||||
|
### User already bound to another Employee
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sys_user": [
|
||||||
|
"User with id 123 is already bound to an existing employee."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### User already has a UserProfile
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user": [
|
||||||
|
"User with id 123 already has a UserProfile."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invalid User ID
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sys_user": [
|
||||||
|
"Invalid pk \"999\" - object does not exist."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission denied
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "无权限创建该对象"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Front-End Implementation Tips
|
||||||
|
|
||||||
|
1. **User selection flow**:
|
||||||
|
- First fetch the list of available Users with active UserProfiles
|
||||||
|
- Then allow users to select from this list when binding to an Employee
|
||||||
|
|
||||||
|
2. **State management**:
|
||||||
|
- After binding, update the UI to reflect the bound status
|
||||||
|
- Ensure consistency between User, UserProfile, and Employee data
|
||||||
|
|
||||||
|
3. **Error handling**:
|
||||||
|
- Implement proper error messages based on the API responses
|
||||||
|
- Distinguish between binding errors and UserProfile management errors
|
||||||
|
|
||||||
|
4. **Data consistency**:
|
||||||
|
- Ensure that the User and Employee belong to the same Merchant
|
||||||
|
- Verify that a User has a UserProfile before attempting to bind to an Employee
|
||||||
|
|
||||||
|
## 8. Example Workflow
|
||||||
|
|
||||||
|
### Full Workflow (Creating User, UserProfile, and Bound Employee)
|
||||||
|
1. **Create User**: First create a User via Django's authentication system.
|
||||||
|
2. **Fetch User ID**: Retrieve the User ID from the response.
|
||||||
|
3. **Create UserProfile**: Create a UserProfile and include the User ID and Merchant ID.
|
||||||
|
4. **Create Employee**: Create an Employee and include the User ID in the `sys_user` field to bind them.
|
||||||
|
5. **Verify binding**: Retrieve the Employee to confirm the User is bound.
|
||||||
|
|
||||||
|
### Partial Workflow (Binding Existing User to Existing Employee)
|
||||||
|
1. **Fetch existing User**: Retrieve User IDs that have active UserProfiles.
|
||||||
|
2. **Fetch existing Employee**: Retrieve the Employee to bind to.
|
||||||
|
3. **Update Employee**: Update the Employee with the User ID to bind them.
|
||||||
|
4. **Verify binding**: Retrieve the Employee to confirm the User is bound.
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from basic_info import models as basic_models
|
from basic_info import models as basic_models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
@@ -159,3 +160,23 @@ class VehicleTransportRecordSerializer(BaseSerializer):
|
|||||||
model = basic_models.VehicleTransportRecord
|
model = basic_models.VehicleTransportRecord
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
depth = 1 # 展开外键关系
|
depth = 1 # 展开外键关系
|
||||||
|
|
||||||
|
|
||||||
|
class UserSerializerSimple(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = basic_models.User
|
||||||
|
fields = ['id', 'username', 'email', 'is_active', 'is_superuser', 'last_login']
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileSerializer(BaseSerializer):
|
||||||
|
user = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=User.objects.all()
|
||||||
|
)
|
||||||
|
user_detail = UserSerializerSimple(source='user', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = basic_models.UserProfile
|
||||||
|
fields = ['id', 'user', 'user_detail', 'merchant', 'description', 'created_at', 'updated_at']
|
||||||
|
extra_kwargs = {
|
||||||
|
'merchant': {'required': False} # 创建时由视图层设置
|
||||||
|
}
|
||||||
|
|||||||
153
api_man/tests.py
153
api_man/tests.py
@@ -303,3 +303,156 @@ class EmployeeAPITestCase(TestCase):
|
|||||||
response = self.client.patch(f'/api/backend/employees/{employee.id}/', data, format='json')
|
response = self.client.patch(f'/api/backend/employees/{employee.id}/', data, format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data['job_type'], '打纸工') # 应该保持不变
|
self.assertEqual(response.data['job_type'], '打纸工') # 应该保持不变
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileAPITestCase(TestCase):
|
||||||
|
"""测试用户资料 API"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""设置测试数据"""
|
||||||
|
# 创建商户
|
||||||
|
self.merchant = Merchant.objects.create(
|
||||||
|
name='测试商户',
|
||||||
|
type=MerchantTypeEnum.STORE
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建用户和员工
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username='testuser',
|
||||||
|
password='testpass123',
|
||||||
|
email='test@example.com'
|
||||||
|
)
|
||||||
|
self.employee = Employee.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
sys_user=self.user,
|
||||||
|
name='测试员工'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建另一个用户用于测试
|
||||||
|
self.user2 = User.objects.create_user(
|
||||||
|
username='testuser2',
|
||||||
|
password='testpass123',
|
||||||
|
email='test2@example.com'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 设置 API 客户端
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
|
def test_create_user_profile(self):
|
||||||
|
"""测试创建用户资料"""
|
||||||
|
data = {
|
||||||
|
'user': self.user2.id,
|
||||||
|
'description': '测试用户资料描述'
|
||||||
|
}
|
||||||
|
response = self.client.post('/api/backend/user-profiles/', data, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(response.data['user'], self.user2.id)
|
||||||
|
self.assertEqual(response.data['merchant'], self.merchant.id) # 应该由BaseViewSet自动设置
|
||||||
|
self.assertEqual(response.data['description'], '测试用户资料描述')
|
||||||
|
|
||||||
|
def test_list_user_profiles(self):
|
||||||
|
"""测试列出所有用户资料"""
|
||||||
|
from basic_info.models import UserProfile
|
||||||
|
UserProfile.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
merchant=self.merchant,
|
||||||
|
description='用户1资料'
|
||||||
|
)
|
||||||
|
UserProfile.objects.create(
|
||||||
|
user=self.user2,
|
||||||
|
merchant=self.merchant,
|
||||||
|
description='用户2资料'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get('/api/backend/user-profiles/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
results = response.data['results'] if isinstance(response.data, dict) else response.data
|
||||||
|
self.assertEqual(len(results), 2) # 应该只返回当前商户的用户资料
|
||||||
|
|
||||||
|
def test_retrieve_user_profile(self):
|
||||||
|
"""测试检索单个用户资料"""
|
||||||
|
from basic_info.models import UserProfile
|
||||||
|
profile = UserProfile.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
merchant=self.merchant,
|
||||||
|
description='测试资料'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(f'/api/backend/user-profiles/{profile.id}/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data['id'], profile.id)
|
||||||
|
self.assertEqual(response.data['description'], '测试资料')
|
||||||
|
self.assertEqual(response.data['user_detail']['username'], 'testuser') # 检查嵌套用户对象
|
||||||
|
|
||||||
|
def test_update_user_profile(self):
|
||||||
|
"""测试更新用户资料"""
|
||||||
|
from basic_info.models import UserProfile
|
||||||
|
profile = UserProfile.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
merchant=self.merchant,
|
||||||
|
description='原始描述'
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {'description': '更新后的描述'}
|
||||||
|
response = self.client.patch(f'/api/backend/user-profiles/{profile.id}/', data, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data['description'], '更新后的描述')
|
||||||
|
|
||||||
|
# 验证数据库
|
||||||
|
profile.refresh_from_db()
|
||||||
|
self.assertEqual(profile.description, '更新后的描述')
|
||||||
|
|
||||||
|
def test_delete_user_profile(self):
|
||||||
|
"""测试删除用户资料"""
|
||||||
|
from basic_info.models import UserProfile
|
||||||
|
profile = UserProfile.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
merchant=self.merchant,
|
||||||
|
description='待删除资料'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.delete(f'/api/backend/user-profiles/{profile.id}/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
self.assertFalse(UserProfile.objects.filter(id=profile.id).exists())
|
||||||
|
|
||||||
|
def test_user_profile_filter_by_merchant(self):
|
||||||
|
"""测试用户资料按商户过滤"""
|
||||||
|
# 创建另一个商户
|
||||||
|
other_merchant = Merchant.objects.create(
|
||||||
|
name='其他商户',
|
||||||
|
type=MerchantTypeEnum.FACTORY
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建另一个商户的员工
|
||||||
|
other_user = User.objects.create_user(
|
||||||
|
username='otheruser',
|
||||||
|
password='testpass123'
|
||||||
|
)
|
||||||
|
other_employee = Employee.objects.create(
|
||||||
|
merchant=other_merchant,
|
||||||
|
sys_user=other_user,
|
||||||
|
name='其他员工'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 为两个商户分别创建用户资料
|
||||||
|
from basic_info.models import UserProfile
|
||||||
|
UserProfile.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
merchant=self.merchant,
|
||||||
|
description='本商户用户资料'
|
||||||
|
)
|
||||||
|
UserProfile.objects.create(
|
||||||
|
user=other_user,
|
||||||
|
merchant=other_merchant,
|
||||||
|
description='其他商户用户资料'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 使用当前用户(属于self.merchant)访问API
|
||||||
|
response = self.client.get('/api/backend/user-profiles/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
results = response.data['results'] if isinstance(response.data, dict) else response.data
|
||||||
|
self.assertEqual(len(results), 1) # 只应该返回当前商户的用户资料
|
||||||
|
self.assertEqual(results[0]['description'], '本商户用户资料')
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ device_info_router.register(prefix='', viewset=views.DeviceInfoViewSet)
|
|||||||
vehicle_transport_record_router = routers.DefaultRouter()
|
vehicle_transport_record_router = routers.DefaultRouter()
|
||||||
vehicle_transport_record_router.register(prefix='', viewset=views.VehicleTransportRecordViewSet)
|
vehicle_transport_record_router.register(prefix='', viewset=views.VehicleTransportRecordViewSet)
|
||||||
|
|
||||||
|
# UserProfile router
|
||||||
|
user_profile_router = routers.DefaultRouter()
|
||||||
|
user_profile_router.register(prefix='', viewset=views.UserProfileViewSet)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('quick-inputs/', include(quick_input_router.urls)),
|
path('quick-inputs/', include(quick_input_router.urls)),
|
||||||
@@ -52,4 +55,5 @@ urlpatterns = [
|
|||||||
path('bank-accounts/', include(bank_account_router.urls)),
|
path('bank-accounts/', include(bank_account_router.urls)),
|
||||||
path('device-info/', include(device_info_router.urls)),
|
path('device-info/', include(device_info_router.urls)),
|
||||||
path('vehicle-transport-records/', include(vehicle_transport_record_router.urls)),
|
path('vehicle-transport-records/', include(vehicle_transport_record_router.urls)),
|
||||||
]
|
path('user-profiles/', include(user_profile_router.urls)),
|
||||||
|
]
|
||||||
@@ -99,3 +99,15 @@ class DeviceInfoViewSet(BaseViewSet):
|
|||||||
class VehicleTransportRecordViewSet(BaseViewSet):
|
class VehicleTransportRecordViewSet(BaseViewSet):
|
||||||
queryset = serializers.basic_models.VehicleTransportRecord.objects
|
queryset = serializers.basic_models.VehicleTransportRecord.objects
|
||||||
serializer_class = serializers.VehicleTransportRecordSerializer
|
serializer_class = serializers.VehicleTransportRecordSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileViewSet(BaseViewSet):
|
||||||
|
queryset = serializers.basic_models.UserProfile.objects
|
||||||
|
serializer_class = serializers.UserProfileSerializer
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
try:
|
||||||
|
merchant = self.request.user.employee.merchant
|
||||||
|
return super().filter_queryset(queryset).filter(merchant=merchant)
|
||||||
|
except AttributeError:
|
||||||
|
raise PermissionDenied("无权限访问该对象")
|
||||||
|
|||||||
19
api_v1/migrations/0002_alter_uploadedfile_path.py
Normal file
19
api_v1/migrations/0002_alter_uploadedfile_path.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-24 03:07
|
||||||
|
|
||||||
|
import api_v1.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api_v1', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='uploadedfile',
|
||||||
|
name='path',
|
||||||
|
field=models.FileField(help_text='文件存储路径,包含前缀、随机文件名和后缀', max_length=500, upload_to=api_v1.models.upload_file_path, verbose_name='文件路径'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -38,6 +38,11 @@ class QuickInputAdmin(admin.ModelAdmin):
|
|||||||
list_filter = ('group',)
|
list_filter = ('group',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.UserProfile)
|
||||||
|
class UserProfileAdmin(AdminBase):
|
||||||
|
list_display = ('user', 'merchant')
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Merchant)
|
@admin.register(models.Merchant)
|
||||||
class MerchantAdmin(admin.ModelAdmin):
|
class MerchantAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'type', 'email', 'mobile', 'auto_complete_stock_change')
|
list_display = ('name', 'type', 'email', 'mobile', 'auto_complete_stock_change')
|
||||||
|
|||||||
31
basic_info/migrations/0012_userprofile.py
Normal file
31
basic_info/migrations/0012_userprofile.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-24 03:07
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('basic_info', '0011_remove_employee_job_type_employeetype_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserProfile',
|
||||||
|
fields=[
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('description', models.TextField(blank=True, null=True, verbose_name='备注描述')),
|
||||||
|
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='user_profiles', to='basic_info.merchant', verbose_name='所属商户')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '用户资料扩展',
|
||||||
|
'verbose_name_plural': '用户资料扩展',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -376,6 +376,30 @@ class Employee(ModelBase):
|
|||||||
verbose_name_plural = '员工资料'
|
verbose_name_plural = '员工资料'
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfile(ModelBase):
|
||||||
|
id = models.BigAutoField(primary_key=True)
|
||||||
|
user = models.OneToOneField(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='profile',
|
||||||
|
verbose_name='关联用户',
|
||||||
|
)
|
||||||
|
merchant = models.ForeignKey(
|
||||||
|
'Merchant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='user_profiles',
|
||||||
|
verbose_name='所属商户',
|
||||||
|
)
|
||||||
|
description = models.TextField(blank=True, null=True, verbose_name='备注描述')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username}'s Profile"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = '用户资料扩展'
|
||||||
|
verbose_name_plural = '用户资料扩展'
|
||||||
|
|
||||||
|
|
||||||
class DeviceInfo(ModelBase):
|
class DeviceInfo(ModelBase):
|
||||||
id = models.BigAutoField(primary_key=True)
|
id = models.BigAutoField(primary_key=True)
|
||||||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='devices', verbose_name='所属商户')
|
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='devices', verbose_name='所属商户')
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
18
stock/migrations/0004_alter_stockchangerecord_source_type.py
Normal file
18
stock/migrations/0004_alter_stockchangerecord_source_type.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-24 03:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0003_remove_inventory_minimum_quantity'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockchangerecord',
|
||||||
|
name='source_type',
|
||||||
|
field=models.IntegerField(choices=[(1, '采购'), (2, '销退'), (3, '调入'), (4, '盘盈'), (5, '合并'), (6, '销售'), (7, '采购退货'), (8, '调出'), (9, '盘亏'), (10, '拆卷')], verbose_name='变动来源'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user