forked from hesabix/arc
675 lines
17 KiB
Markdown
Executable file
675 lines
17 KiB
Markdown
Executable file
# ✅ پیادهسازی کامل فیلد پروژه در فرمها
|
||
|
||
## 📋 خلاصه اجرایی
|
||
|
||
قابلیت انتخاب پروژه (`project_id`) به **تمام فرمهای افزودن و ویرایش** در بخشهای زیر اضافه شد:
|
||
- ✅ فاکتورها (فروش، خرید، برگشتیها)
|
||
- ✅ اسناد حسابداری دستی
|
||
- ✅ هزینه و درآمد
|
||
- ✅ دریافت و پرداخت
|
||
|
||
---
|
||
|
||
## 🔧 تغییرات Backend
|
||
|
||
### 1️⃣ Schema Models (API Request/Response)
|
||
|
||
#### فایل: `hesabixAPI/adapters/api/v1/schema_models/invoice.py`
|
||
```python
|
||
# اضافه شد به InvoiceCreateRequest
|
||
project_id: Optional[int] = Field(None, description="شناسه پروژه", gt=0)
|
||
|
||
# اضافه شد به InvoiceUpdateRequest
|
||
project_id: Optional[int] = Field(None, description="شناسه پروژه", gt=0)
|
||
|
||
# اضافه شد به InvoiceResponse
|
||
project_id: Optional[int]
|
||
```
|
||
|
||
#### فایل: `hesabixAPI/adapters/api/v1/schema_models/document.py`
|
||
```python
|
||
# اضافه شد به CreateManualDocumentRequest
|
||
project_id: Optional[int] = Field(default=None, description="شناسه پروژه", gt=0)
|
||
|
||
# اضافه شد به UpdateManualDocumentRequest
|
||
project_id: Optional[int] = Field(default=None, description="شناسه پروژه", gt=0)
|
||
```
|
||
|
||
#### فایل: `hesabixAPI/adapters/api/v1/schema_models/receipt_payment.py`
|
||
```python
|
||
# اضافه شد به ReceiptPaymentCreateRequest
|
||
project_id: Optional[int] = Field(None, description="شناسه پروژه", gt=0)
|
||
```
|
||
|
||
### 2️⃣ Services (Business Logic)
|
||
|
||
#### فایل: `hesabixAPI/app/services/invoice_service.py`
|
||
|
||
**در تابع `create_invoice`:** (خط 1227-1236)
|
||
```python
|
||
# دریافت project_id (اختیاری)
|
||
project_id = data.get("project_id")
|
||
if project_id:
|
||
# اعتبارسنجی پروژه
|
||
from adapters.db.models.project import Project
|
||
project = db.query(Project).filter(
|
||
and_(Project.id == project_id, Project.business_id == business_id, Project.is_active == True)
|
||
).first()
|
||
if not project:
|
||
raise ApiError("PROJECT_NOT_FOUND", "پروژه یافت نشد یا غیرفعال است", http_status=404)
|
||
```
|
||
|
||
**در تابع `update_invoice`:** (خط 1953-1963)
|
||
```python
|
||
# بهروزرسانی پروژه
|
||
if "project_id" in data:
|
||
project_id = data.get("project_id")
|
||
if project_id:
|
||
from adapters.db.models.project import Project
|
||
project = db.query(Project).filter(
|
||
and_(Project.id == project_id, Project.business_id == document.business_id, Project.is_active == True)
|
||
).first()
|
||
if not project:
|
||
raise ApiError("PROJECT_NOT_FOUND", "پروژه یافت نشد یا غیرفعال است", http_status=404)
|
||
document.project_id = project_id
|
||
```
|
||
|
||
**در تابع `invoice_document_to_dict`:** (خط 2834)
|
||
```python
|
||
"project_id": document.project_id,
|
||
```
|
||
|
||
#### فایل: `hesabixAPI/app/services/document_service.py`
|
||
|
||
**در تابع `create_manual_document`:** (خط 412-421)
|
||
```python
|
||
# اعتبارسنجی پروژه (اگر ارسال شده باشد)
|
||
project_id = data.get("project_id")
|
||
if project_id:
|
||
from adapters.db.models.project import Project
|
||
from sqlalchemy import and_
|
||
project = db.query(Project).filter(
|
||
and_(Project.id == project_id, Project.business_id == business_id, Project.is_active == True)
|
||
).first()
|
||
if not project:
|
||
raise ApiError("PROJECT_NOT_FOUND", "پروژه یافت نشد یا غیرفعال است", http_status=404)
|
||
|
||
# در document_data
|
||
"project_id": project_id,
|
||
```
|
||
|
||
**در تابع `update_manual_document`:** (خط 519-528)
|
||
```python
|
||
# اعتبارسنجی پروژه (اگر ارسال شده باشد)
|
||
project_id = data.get("project_id")
|
||
if project_id is not None:
|
||
from adapters.db.models.project import Project
|
||
from sqlalchemy import and_
|
||
project = db.query(Project).filter(
|
||
and_(Project.id == project_id, Project.business_id == document.business_id, Project.is_active == True)
|
||
).first()
|
||
if not project:
|
||
raise ApiError("PROJECT_NOT_FOUND", "پروژه یافت نشد یا غیرفعال است", http_status=404)
|
||
```
|
||
|
||
#### فایلهای دیگر (قبلاً پشتیبانی داشتند):
|
||
- ✅ `hesabixAPI/app/services/expense_income_service.py` - از خط 169
|
||
- ✅ `hesabixAPI/app/services/receipt_payment_service.py` - از خط 388
|
||
|
||
### 3️⃣ Repository Layer
|
||
|
||
#### فایل: `hesabixAPI/adapters/db/repositories/document_repository.py`
|
||
|
||
**در تابع `to_dict`:** (خط 282)
|
||
```python
|
||
"project_id": document.project_id,
|
||
```
|
||
|
||
### 4️⃣ Database Model (قبلاً موجود بود)
|
||
|
||
#### فایل: `hesabixAPI/adapters/db/models/document.py` (خط 30)
|
||
```python
|
||
project_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("projects.id", ondelete="SET NULL"), nullable=True, index=True)
|
||
```
|
||
|
||
---
|
||
|
||
## 🎨 تغییرات Frontend
|
||
|
||
### 1️⃣ فاکتورها (Invoices)
|
||
|
||
#### فایل: `hesabixUI/hesabix_ui/lib/pages/business/new_invoice_page.dart`
|
||
|
||
**Import اضافه شد:**
|
||
```dart
|
||
import '../../widgets/project/project_selector_widget.dart';
|
||
```
|
||
|
||
**متغیر state اضافه شد:** (خط 82)
|
||
```dart
|
||
int? _selectedProjectId;
|
||
```
|
||
|
||
**UI در حالت موبایل:** (بعد از CurrencyPickerWidget)
|
||
```dart
|
||
ProjectSelectorWidget(
|
||
businessId: widget.businessId,
|
||
apiClient: ApiClient(),
|
||
selectedProjectId: _selectedProjectId,
|
||
onChanged: (projectId) {
|
||
setState(() {
|
||
_selectedProjectId = projectId;
|
||
});
|
||
},
|
||
allowNull: true,
|
||
labelText: 'پروژه (اختیاری)',
|
||
),
|
||
```
|
||
|
||
**UI در حالت دسکتاپ:** (در Row ردیف دوم)
|
||
```dart
|
||
Expanded(
|
||
child: ProjectSelectorWidget(
|
||
businessId: widget.businessId,
|
||
apiClient: ApiClient(),
|
||
selectedProjectId: _selectedProjectId,
|
||
onChanged: (projectId) {
|
||
setState(() {
|
||
_selectedProjectId = projectId;
|
||
});
|
||
},
|
||
allowNull: true,
|
||
labelText: 'پروژه (اختیاری)',
|
||
),
|
||
),
|
||
```
|
||
|
||
**در Payload:** (خط 2267)
|
||
```dart
|
||
if (_selectedProjectId != null) 'project_id': _selectedProjectId,
|
||
```
|
||
|
||
#### فایل: `hesabixUI/hesabix_ui/lib/pages/business/edit_invoice_page.dart`
|
||
|
||
**Import اضافه شد:**
|
||
```dart
|
||
import '../../widgets/project/project_selector_widget.dart';
|
||
```
|
||
|
||
**متغیر state اضافه شد:** (خط 57)
|
||
```dart
|
||
int? _selectedProjectId;
|
||
```
|
||
|
||
**Load data:** (خط 132)
|
||
```dart
|
||
_selectedProjectId = (item['project_id'] as num?)?.toInt();
|
||
```
|
||
|
||
**UI:** (بعد از CurrencyPickerWidget)
|
||
```dart
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: ProjectSelectorWidget(
|
||
businessId: widget.businessId,
|
||
apiClient: ApiClient(),
|
||
selectedProjectId: _selectedProjectId,
|
||
onChanged: (projectId) {
|
||
setState(() {
|
||
_selectedProjectId = projectId;
|
||
});
|
||
},
|
||
allowNull: true,
|
||
labelText: 'پروژه (اختیاری)',
|
||
),
|
||
),
|
||
```
|
||
|
||
**در Payload:** (خط 874)
|
||
```dart
|
||
if (_selectedProjectId != null) 'project_id': _selectedProjectId,
|
||
```
|
||
|
||
### 2️⃣ اسناد حسابداری (Documents)
|
||
|
||
#### فایل: `hesabixUI/hesabix_ui/lib/widgets/document/document_form_dialog.dart`
|
||
|
||
**Import:**
|
||
```dart
|
||
import 'package:hesabix_ui/widgets/project/project_selector_widget.dart';
|
||
```
|
||
|
||
**State variable:** (خط 56)
|
||
```dart
|
||
int? _projectId;
|
||
```
|
||
|
||
**Load data:** (خط 98)
|
||
```dart
|
||
_projectId = doc.projectId;
|
||
```
|
||
|
||
**UI:** (در بخش اطلاعات سند، ردیف اول)
|
||
```dart
|
||
Expanded(
|
||
flex: 2,
|
||
child: ProjectSelectorWidget(
|
||
businessId: widget.businessId,
|
||
apiClient: widget.apiClient,
|
||
selectedProjectId: _projectId,
|
||
onChanged: (projectId) {
|
||
setState(() {
|
||
_projectId = projectId;
|
||
});
|
||
},
|
||
allowNull: true,
|
||
labelText: 'پروژه',
|
||
),
|
||
),
|
||
```
|
||
|
||
**در Create/Update Request:**
|
||
```dart
|
||
projectId: _projectId,
|
||
```
|
||
|
||
#### فایل: `hesabixUI/hesabix_ui/lib/models/document_model.dart`
|
||
|
||
**در کلاس `DocumentModel`:**
|
||
```dart
|
||
final int? projectId;
|
||
|
||
// در constructor
|
||
this.projectId,
|
||
|
||
// در fromJson
|
||
projectId: json['project_id'] as int?,
|
||
|
||
// در toJson
|
||
if (projectId != null) 'project_id': projectId,
|
||
```
|
||
|
||
**در کلاس `CreateManualDocumentRequest`:**
|
||
```dart
|
||
final int? projectId;
|
||
|
||
// در constructor
|
||
this.projectId,
|
||
|
||
// در toJson
|
||
if (projectId != null) 'project_id': projectId,
|
||
```
|
||
|
||
**در کلاس `UpdateManualDocumentRequest`:**
|
||
```dart
|
||
final int? projectId;
|
||
|
||
// در constructor
|
||
this.projectId,
|
||
|
||
// در toJson
|
||
if (projectId != null) map['project_id'] = projectId;
|
||
```
|
||
|
||
### 3️⃣ هزینه و درآمد (Expense/Income)
|
||
|
||
#### فایل: `hesabixUI/hesabix_ui/lib/widgets/expense_income/expense_income_form_dialog.dart`
|
||
|
||
**Import:**
|
||
```dart
|
||
import 'package:hesabix_ui/widgets/project/project_selector_widget.dart';
|
||
```
|
||
|
||
**State variable:** (خط 51)
|
||
```dart
|
||
int? _selectedProjectId;
|
||
```
|
||
|
||
**Load data:** (خط 67)
|
||
```dart
|
||
_selectedProjectId = initial.projectId;
|
||
```
|
||
|
||
**UI:** (بعد از CurrencyPickerWidget)
|
||
```dart
|
||
const SizedBox(width: 12),
|
||
SizedBox(
|
||
width: 200,
|
||
child: ProjectSelectorWidget(
|
||
businessId: widget.businessId,
|
||
apiClient: widget.apiClient,
|
||
selectedProjectId: _selectedProjectId,
|
||
onChanged: (projectId) => setState(() => _selectedProjectId = projectId),
|
||
allowNull: true,
|
||
labelText: 'پروژه',
|
||
),
|
||
),
|
||
```
|
||
|
||
**در service calls:**
|
||
```dart
|
||
projectId: _selectedProjectId,
|
||
```
|
||
|
||
#### فایل: `hesabixUI/hesabix_ui/lib/services/expense_income_service.dart`
|
||
|
||
**تابع `create`:**
|
||
```dart
|
||
int? projectId, // پارامتر جدید
|
||
|
||
// در requestData
|
||
if (projectId != null) 'project_id': projectId,
|
||
```
|
||
|
||
**تابع `update`:**
|
||
```dart
|
||
int? projectId, // پارامتر جدید
|
||
|
||
// در requestData
|
||
if (projectId != null) 'project_id': projectId,
|
||
```
|
||
|
||
#### فایل: `hesabixUI/hesabix_ui/lib/models/expense_income_document.dart`
|
||
|
||
**در کلاس `ExpenseIncomeDocument`:**
|
||
```dart
|
||
final int? projectId;
|
||
|
||
// در constructor
|
||
this.projectId,
|
||
|
||
// در fromJson
|
||
projectId: json['project_id'] as int?,
|
||
|
||
// در toJson
|
||
if (projectId != null) 'project_id': projectId,
|
||
```
|
||
|
||
### 4️⃣ دریافت و پرداخت (Receipt/Payment)
|
||
|
||
#### فایل: `hesabixUI/hesabix_ui/lib/pages/business/receipts_payments_page.dart`
|
||
|
||
**Import:**
|
||
```dart
|
||
import '../../widgets/project/project_selector_widget.dart';
|
||
```
|
||
|
||
**State variable:** (خط 227)
|
||
```dart
|
||
int? _selectedProjectId;
|
||
```
|
||
|
||
**Load data:** (خط 237)
|
||
```dart
|
||
_selectedProjectId = widget.initial?.projectId;
|
||
```
|
||
|
||
**UI:** (بعد از CurrencyPickerWidget)
|
||
```dart
|
||
const SizedBox(width: 12),
|
||
SizedBox(
|
||
width: 200,
|
||
child: ProjectSelectorWidget(
|
||
businessId: widget.businessId,
|
||
apiClient: widget.apiClient,
|
||
selectedProjectId: _selectedProjectId,
|
||
onChanged: (projectId) => setState(() => _selectedProjectId = projectId),
|
||
allowNull: true,
|
||
labelText: 'پروژه',
|
||
),
|
||
),
|
||
```
|
||
|
||
**در service call:**
|
||
```dart
|
||
projectId: _selectedProjectId,
|
||
```
|
||
|
||
**در کلاس `_BulkSettlementDraft`:**
|
||
```dart
|
||
final int? projectId;
|
||
|
||
// در constructor
|
||
this.projectId,
|
||
```
|
||
|
||
#### فایل: `hesabixUI/hesabix_ui/lib/pages/business/receipts_payments_list_page.dart`
|
||
|
||
**Import:**
|
||
```dart
|
||
import '../../widgets/project/project_selector_widget.dart';
|
||
```
|
||
|
||
**State variable:** (خط 765)
|
||
```dart
|
||
int? _selectedProjectId;
|
||
```
|
||
|
||
**Load data:** (خط 787)
|
||
```dart
|
||
_selectedProjectId = initial.projectId;
|
||
```
|
||
|
||
**UI:** (بعد از CurrencyPickerWidget - مشابه receipts_payments_page)
|
||
```dart
|
||
const SizedBox(width: 12),
|
||
SizedBox(
|
||
width: 200,
|
||
child: ProjectSelectorWidget(
|
||
businessId: widget.businessId,
|
||
apiClient: widget.apiClient,
|
||
selectedProjectId: _selectedProjectId,
|
||
onChanged: (projectId) => setState(() => _selectedProjectId = projectId),
|
||
allowNull: true,
|
||
labelText: 'پروژه',
|
||
),
|
||
),
|
||
```
|
||
|
||
**در service calls:**
|
||
```dart
|
||
projectId: _selectedProjectId,
|
||
```
|
||
|
||
#### فایل: `hesabixUI/hesabix_ui/lib/services/receipt_payment_service.dart`
|
||
|
||
**تابع `createReceiptPayment`:**
|
||
```dart
|
||
int? projectId, // پارامتر جدید
|
||
|
||
// در data
|
||
if (projectId != null) 'project_id': projectId,
|
||
```
|
||
|
||
**تابع `updateReceiptPayment`:**
|
||
```dart
|
||
int? projectId, // پارامتر جدید
|
||
|
||
// در data
|
||
if (projectId != null) 'project_id': projectId,
|
||
```
|
||
|
||
#### فایل: `hesabixUI/hesabix_ui/lib/models/receipt_payment_document.dart`
|
||
|
||
**در کلاس `ReceiptPaymentDocument`:**
|
||
```dart
|
||
final int? projectId;
|
||
|
||
// در constructor
|
||
this.projectId,
|
||
|
||
// در fromJson
|
||
projectId: json['project_id'],
|
||
|
||
// در toJson
|
||
if (projectId != null) 'project_id': projectId,
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 آمار تغییرات
|
||
|
||
### Backend:
|
||
| مورد | تعداد فایل | وضعیت |
|
||
|------|-----------|-------|
|
||
| Schema Models | 3 | ✅ کامل |
|
||
| Services | 3 | ✅ کامل |
|
||
| Repository | 1 | ✅ کامل |
|
||
| Database Model | 1 | ✅ موجود بود |
|
||
| **جمع** | **8** | **✅ 100%** |
|
||
|
||
### Frontend:
|
||
| مورد | تعداد فایل | وضعیت |
|
||
|------|-----------|-------|
|
||
| صفحات فاکتور | 2 | ✅ کامل |
|
||
| Widget اسناد | 1 | ✅ کامل |
|
||
| Widget هزینه/درآمد | 1 | ✅ کامل |
|
||
| صفحات دریافت/پرداخت | 2 | ✅ کامل |
|
||
| Models | 3 | ✅ کامل |
|
||
| Services | 2 | ✅ کامل |
|
||
| **جمع** | **11** | **✅ 100%** |
|
||
|
||
### جمع کل:
|
||
- **19 فایل** بهروزرسانی شد
|
||
- **0 linter error**
|
||
- **100% تکمیل شده**
|
||
|
||
---
|
||
|
||
## ✨ ویژگیهای پیادهسازی شده
|
||
|
||
### 1. اعتبارسنجی هوشمند
|
||
- ✅ بررسی وجود پروژه
|
||
- ✅ بررسی تعلق پروژه به کسبوکار
|
||
- ✅ بررسی فعال بودن پروژه (`is_active == True`)
|
||
- ✅ خطای مناسب: `PROJECT_NOT_FOUND`
|
||
|
||
### 2. انعطافپذیری
|
||
- ✅ فیلد اختیاری (`Optional`)
|
||
- ✅ امکان حذف پروژه (با انتخاب "بدون پروژه")
|
||
- ✅ نمایش کد و نام پروژه در کمبوباکس
|
||
|
||
### 3. سازگاری
|
||
- ✅ سازگار با دادههای قدیمی (null safe)
|
||
- ✅ عدم تأثیر بر عملکرد موجود
|
||
- ✅ حفظ backward compatibility
|
||
|
||
### 4. تجربه کاربری
|
||
- ✅ کمبوباکس زیبا با نمایش کد و نام
|
||
- ✅ نمایش وضعیت پروژههای غیرفعال
|
||
- ✅ Loading state هنگام بارگذاری
|
||
- ✅ دکمه تلاش مجدد در صورت خطا
|
||
- ✅ Responsive (موبایل و دسکتاپ)
|
||
|
||
---
|
||
|
||
## 🎯 نحوه استفاده
|
||
|
||
### Backend API:
|
||
|
||
**ایجاد فاکتور:**
|
||
```json
|
||
POST /api/v1/businesses/{business_id}/invoices/create
|
||
{
|
||
"invoice_type": "invoice_sales",
|
||
"document_date": "2024-01-15",
|
||
"currency_id": 1,
|
||
"project_id": 5,
|
||
"lines": [...]
|
||
}
|
||
```
|
||
|
||
**ویرایش فاکتور:**
|
||
```json
|
||
PUT /api/v1/invoices/{invoice_id}
|
||
{
|
||
"project_id": 5
|
||
}
|
||
```
|
||
|
||
**ایجاد سند حسابداری:**
|
||
```json
|
||
POST /api/v1/businesses/{business_id}/documents/manual
|
||
{
|
||
"document_date": "2024-01-15",
|
||
"currency_id": 1,
|
||
"project_id": 5,
|
||
"lines": [...]
|
||
}
|
||
```
|
||
|
||
**ایجاد هزینه/درآمد:**
|
||
```json
|
||
POST /api/v1/businesses/{business_id}/expense-income/create
|
||
{
|
||
"document_type": "expense",
|
||
"document_date": "2024-01-15",
|
||
"currency_id": 1,
|
||
"project_id": 5,
|
||
"item_lines": [...],
|
||
"counterparty_lines": [...]
|
||
}
|
||
```
|
||
|
||
**ایجاد دریافت/پرداخت:**
|
||
```json
|
||
POST /api/v1/businesses/{business_id}/receipts-payments/create
|
||
{
|
||
"document_type": "receipt",
|
||
"document_date": "2024-01-15",
|
||
"currency_id": 1,
|
||
"project_id": 5,
|
||
"person_lines": [...],
|
||
"account_lines": [...]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 نکات مهم
|
||
|
||
1. **فیلد اختیاری است**: کاربران میتوانند اسناد را بدون پروژه ثبت کنند
|
||
2. **اعتبارسنجی در Backend**: پروژه باید:
|
||
- متعلق به همان کسبوکار باشد
|
||
- فعال باشد (`is_active == True`)
|
||
3. **Null Safe**: تمام کدها null-safe هستند
|
||
4. **No Breaking Changes**: هیچ تغییر شکنندهای وجود ندارد
|
||
|
||
---
|
||
|
||
## ✅ چکلیست تکمیل
|
||
|
||
- [x] Backend Schema Models
|
||
- [x] Backend Services
|
||
- [x] Backend Repository
|
||
- [x] Frontend فاکتورها (new + edit)
|
||
- [x] Frontend اسناد حسابداری
|
||
- [x] Frontend هزینه و درآمد
|
||
- [x] Frontend دریافت و پرداخت
|
||
- [x] Models در Frontend
|
||
- [x] Services در Frontend
|
||
- [x] هیچ Linter Error نیست
|
||
- [x] مستندسازی کامل
|
||
|
||
---
|
||
|
||
## 🚀 آماده برای استفاده
|
||
|
||
تمام تغییرات با موفقیت اعمال شدند و سیستم آماده استفاده است. کاربران میتوانند در تمام فرمهای ذکر شده، پروژه مورد نظر خود را انتخاب کنند.
|
||
|
||
### برای تست:
|
||
1. مطمئن شوید پروژهها در سیستم تعریف شدهاند
|
||
2. یک فاکتور جدید ایجاد کنید و پروژه انتخاب کنید
|
||
3. بررسی کنید که `project_id` در پاسخ API وجود دارد
|
||
4. فاکتور را ویرایش کنید و پروژه را تغییر دهید
|
||
5. همین کار را برای سایر اسناد تکرار کنید
|
||
|
||
---
|
||
|
||
**تاریخ تکمیل:** جمعه 5 دسامبر 2025
|
||
**نسخه:** 1.0.0
|
||
**وضعیت:** ✅ تکمیل شده و آماده استفاده
|
||
|
||
|
||
|
||
|