40 KiB
Executable file
📊 سناریوی جامع یکپارچهسازی فیلتر پروژه در گزارشات
🎯 خلاصه اجرایی
افزودن فیلتر پروژه به 18 گزارش موجود در سیستم برای امکان تحلیل و ردیابی مالی هر پروژه به صورت جداگانه.
📋 فهرست کامل گزارشات و اولویتبندی
دسته 1: گزارشات حسابداری محوری (اولویت 1 - بحرانی) 🔴
| ردیف | نام گزارش | فایل Backend | فایل Frontend | اولویت |
|---|---|---|---|---|
| 1 | دفتر کل | general_ledger_service.py |
general_ledger_report_page.dart |
⭐⭐⭐⭐⭐ |
| 2 | دفتر روزنامه | journal_ledger_service.py |
journal_ledger_report_page.dart |
⭐⭐⭐⭐⭐ |
| 3 | تراز آزمایشی | trial_balance_service.py |
trial_balance_report_page.dart |
⭐⭐⭐⭐ |
| 4 | سود و زیان دورهای | pnl_service.py |
pnl_period_report_page.dart |
⭐⭐⭐⭐⭐ |
| 5 | سود و زیان تجمعی | pnl_service.py |
pnl_cumulative_report_page.dart |
⭐⭐⭐⭐⭐ |
دسته 2: گزارشات اشخاص (اولویت 2 - بالا) 🟠
| ردیف | نام گزارش | اولویت |
|---|---|---|
| 6 | تراکنشهای اشخاص | ⭐⭐⭐⭐ |
| 7 | بدهکاران | ⭐⭐⭐⭐ |
| 8 | بستانکاران | ⭐⭐⭐⭐ |
| 9 | برترین مشتریان | ⭐⭐⭐ |
| 10 | برترین تامینکنندگان | ⭐⭐⭐ |
دسته 3: گزارشات فروش و خرید (اولویت 3 - متوسط) 🟡
| ردیف | نام گزارش | اولویت |
|---|---|---|
| 11 | فروش روزانه | ⭐⭐⭐⭐ |
| 12 | خرید روزانه | ⭐⭐⭐⭐ |
| 13 | فروش ماهانه | ⭐⭐⭐ |
| 14 | فروش بر اساس محصول | ⭐⭐⭐ |
دسته 4: گزارشات بانکی و نقدی (اولویت 4 - متوسط) 🟡
| ردیف | نام گزارش | اولویت |
|---|---|---|
| 15 | گردش بانک | ⭐⭐⭐⭐ |
| 16 | گردش صندوق و تنخواه | ⭐⭐⭐⭐ |
دسته 5: گزارشات انبار (اولویت 5 - پایین) 🟢
| ردیف | نام گزارش | اولویت |
|---|---|---|
| 17 | کاردکس موجودی | ⭐⭐⭐ |
| 18 | تحرکات محصول | ⭐⭐ |
🔧 راهنمای پیادهسازی گام به گام
گام 1: Backend - بهروزرسانی سرویسهای گزارش
📄 فایل: general_ledger_service.py
قبل از تغییر:
def get_general_ledger_report(
db: Session,
business_id: int,
account_ids: List[int],
fiscal_year_id: Optional[int] = None,
currency_id: Optional[int] = None,
date_from: Optional[str] = None,
date_to: Optional[str] = None,
person_id: Optional[int] = None,
include_proforma: bool = False,
skip: int = 0,
take: int = 50,
) -> Dict[str, Any]:
بعد از تغییر:
def get_general_ledger_report(
db: Session,
business_id: int,
account_ids: List[int],
fiscal_year_id: Optional[int] = None,
currency_id: Optional[int] = None,
date_from: Optional[str] = None,
date_to: Optional[str] = None,
person_id: Optional[int] = None,
project_id: Optional[int] = None, # 🆕 اضافه شد
include_proforma: bool = False,
skip: int = 0,
take: int = 50,
) -> Dict[str, Any]:
"""
گزارش دفتر کل با فیلتر پروژه
Args:
project_id: شناسه پروژه برای فیلتر (اختیاری)
"""
در بدنه تابع، اضافه کنید:
# در قسمت query building
query = db.query(DocumentLine).join(Document).filter(
Document.business_id == business_id,
# ... سایر شرطها
)
# 🆕 فیلتر پروژه
if project_id:
query = query.filter(Document.project_id == project_id)
همین تغییرات را برای این سرویسها تکرار کنید:
- ✅
journal_ledger_service.py→ تابعget_journal_ledger_report - ✅
trial_balance_service.py→ تابعget_trial_balance_report - ✅
pnl_service.py→ توابعget_pnl_period_reportوget_pnl_cumulative_report
گام 2: Backend - بهروزرسانی API Endpoints
📄 فایل: adapters/api/v1/documents.py
مثال: endpoint دفتر کل
قبل از تغییر:
@router.post("/businesses/{business_id}/reports/general-ledger")
async def general_ledger_report_endpoint(
request: Request,
business_id: int,
body: Dict[str, Any] = Body(default={}),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
):
# ...
date_from = body.get('date_from')
date_to = body.get('date_to')
currency_id = body.get('currency_id')
person_id = body.get('person_id')
include_proforma = body.get('include_proforma', False)
بعد از تغییر:
@router.post("/businesses/{business_id}/reports/general-ledger")
async def general_ledger_report_endpoint(
request: Request,
business_id: int,
body: Dict[str, Any] = Body(default={}),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
):
# ...
date_from = body.get('date_from')
date_to = body.get('date_to')
currency_id = body.get('currency_id')
person_id = body.get('person_id')
project_id = body.get('project_id') # 🆕 اضافه شد
include_proforma = body.get('include_proforma', False)
# 🆕 اعتبارسنجی پروژه
if project_id is not None:
try:
project_id = int(project_id)
except (ValueError, TypeError):
project_id = None
result = get_general_ledger_report(
db=db,
business_id=business_id,
account_ids=account_ids,
fiscal_year_id=fiscal_year_id,
currency_id=currency_id,
date_from=date_from,
date_to=date_to,
person_id=person_id,
project_id=project_id, # 🆕 پاس دادن به سرویس
include_proforma=include_proforma,
skip=skip,
take=take,
)
این تغییرات را برای این endpoint ها اعمال کنید:
- ✅
/reports/general-ledger(خط 2230) - ✅
/reports/pnl-period(خط 2335) - ✅
/reports/pnl-cumulative(خط 2414) - ✅
/reports/journal-ledger(اگر وجود دارد) - ✅
/reports/trial-balance(خط 2125)
گام 3: Frontend - ایجاد Widget فیلتر یکپارچه
📄 فایل: lib/widgets/reports/common_report_filters.dart
import 'package:flutter/material.dart';
import 'package:hesabix_ui/core/api_client.dart';
import 'package:hesabix_ui/core/calendar_controller.dart';
import 'package:hesabix_ui/widgets/date_input_field.dart';
import 'package:hesabix_ui/widgets/project/project_selector_widget.dart';
/// فیلترهای مشترک گزارشات مالی
class CommonReportFilters extends StatelessWidget {
final int businessId;
final ApiClient apiClient;
final CalendarController calendarController;
// فیلترهای تاریخ
final DateTime? fromDate;
final DateTime? toDate;
final VoidCallback? onClearDates;
final Function(DateTime?) onFromDateChanged;
final Function(DateTime?) onToDateChanged;
// فیلتر سال مالی
final int? selectedFiscalYearId;
final List<Map<String, dynamic>>? fiscalYears;
final Function(int?)? onFiscalYearChanged;
// فیلتر پروژه 🆕
final int? selectedProjectId;
final Function(int?) onProjectChanged;
// نمایش/عدم نمایش فیلترها
final bool showDateFilters;
final bool showFiscalYearFilter;
final bool showProjectFilter;
const CommonReportFilters({
Key? key,
required this.businessId,
required this.apiClient,
required this.calendarController,
this.fromDate,
this.toDate,
this.onClearDates,
required this.onFromDateChanged,
required this.onToDateChanged,
this.selectedFiscalYearId,
this.fiscalYears,
this.onFiscalYearChanged,
required this.selectedProjectId,
required this.onProjectChanged,
this.showDateFilters = true,
this.showFiscalYearFilter = true,
this.showProjectFilter = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Wrap(
spacing: 12,
runSpacing: 12,
children: [
// فیلترهای تاریخ
if (showDateFilters) ...[
SizedBox(
width: 200,
child: DateInputField(
value: fromDate,
calendarController: calendarController,
onChanged: onFromDateChanged,
labelText: 'از تاریخ',
hintText: 'انتخاب تاریخ',
),
),
SizedBox(
width: 200,
child: DateInputField(
value: toDate,
calendarController: calendarController,
onChanged: onToDateChanged,
labelText: 'تا تاریخ',
hintText: 'انتخاب تاریخ',
),
),
if (onClearDates != null)
IconButton(
onPressed: onClearDates,
icon: const Icon(Icons.clear),
tooltip: 'پاک کردن فیلتر تاریخ',
),
],
// فیلتر سال مالی
if (showFiscalYearFilter && fiscalYears != null && fiscalYears!.isNotEmpty)
SizedBox(
width: 220,
child: DropdownButtonFormField<int>(
value: selectedFiscalYearId,
decoration: const InputDecoration(
labelText: 'سال مالی',
border: OutlineInputBorder(),
isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 16),
),
items: fiscalYears!.map((fy) {
final id = fy['id'] as int;
final title = fy['title'] as String? ?? 'FY $id';
return DropdownMenuItem<int>(
value: id,
child: Text(title, overflow: TextOverflow.ellipsis),
);
}).toList(),
onChanged: onFiscalYearChanged,
),
),
// 🆕 فیلتر پروژه
if (showProjectFilter)
SizedBox(
width: 280,
child: ProjectSelectorWidget(
businessId: businessId,
apiClient: apiClient,
selectedProjectId: selectedProjectId,
onChanged: onProjectChanged,
allowNull: true,
labelText: 'پروژه (همه)',
),
),
],
),
);
}
}
گام 4: Frontend - بهروزرسانی صفحه دفتر کل
📄 فایل: general_ledger_report_page.dart
تغییرات مورد نیاز:
1. اضافه کردن Import
import 'package:hesabix_ui/widgets/reports/common_report_filters.dart';
2. اضافه کردن State
class _GeneralLedgerReportPageState extends State<GeneralLedgerReportPage> {
// ... سایر state ها
int? _selectedProjectId; // 🆕 اضافه کنید
3. جایگزینی بخش فیلترها
قبل:
// فیلترهای دستی موجود
Row(
children: [
DateInputField(...),
DateInputField(...),
// ...
],
)
بعد:
// 🆕 استفاده از widget مشترک
CommonReportFilters(
businessId: widget.businessId,
apiClient: ApiClient(),
calendarController: widget.calendarController,
fromDate: _fromDate,
toDate: _toDate,
onFromDateChanged: (date) {
setState(() => _fromDate = date);
_refreshData();
},
onToDateChanged: (date) {
setState(() => _toDate = date);
_refreshData();
},
onClearDates: () {
setState(() {
_fromDate = null;
_toDate = null;
});
_refreshData();
},
selectedFiscalYearId: _selectedFiscalYearId,
fiscalYears: _fiscalYears,
onFiscalYearChanged: (fyId) {
setState(() => _selectedFiscalYearId = fyId);
_refreshData();
},
selectedProjectId: _selectedProjectId, // 🆕
onProjectChanged: (projectId) { // 🆕
setState(() => _selectedProjectId = projectId);
_refreshData();
},
),
4. اضافه کردن به DataTableConfig
DataTableConfig(
endpoint: '/api/v1/businesses/${widget.businessId}/reports/general-ledger',
method: 'POST',
requestBody: () {
final body = <String, dynamic>{
'account_ids': _selectedAccounts.map((a) => a.id).toList(),
};
if (_fromDate != null) body['date_from'] = _fromDate!.toIso8601String();
if (_toDate != null) body['date_to'] = _toDate!.toIso8601String();
if (_selectedFiscalYearId != null) body['fiscal_year_id'] = _selectedFiscalYearId;
if (_selectedCurrencyId != null) body['currency_id'] = _selectedCurrencyId;
if (_selectedPerson != null) body['person_id'] = _selectedPerson!.id;
if (_selectedProjectId != null) body['project_id'] = _selectedProjectId; // 🆕
body['include_proforma'] = _includeProforma;
return body;
},
// ...
)
گام 5: نمونه کامل برای یک گزارش
🎯 مثال عملی: گزارش سود و زیان
Backend
1. بهروزرسانی Service:
# hesabixAPI/app/services/pnl_service.py
def get_pnl_period_report(
db: Session,
business_id: int,
fiscal_year_id: Optional[int] = None,
currency_id: Optional[int] = None,
date_from: Optional[str] = None,
date_to: Optional[str] = None,
project_id: Optional[int] = None, # 🆕 خط 51
skip: int = 0,
take: int = 100,
) -> Dict[str, Any]:
"""
گزارش سود و زیان دورهای با فیلتر پروژه
"""
# ... کد موجود ...
# 🆕 اضافه کردن فیلتر پروژه در query های درآمد و هزینه
turnover_query = db.query(
DocumentLine.account_id,
func.sum(DocumentLine.debit).label('total_debit'),
func.sum(DocumentLine.credit).label('total_credit')
).join(
Document, DocumentLine.document_id == Document.id
).filter(
and_(
Document.business_id == business_id,
Document.is_proforma == False,
DocumentLine.account_id.isnot(None),
DocumentLine.account_id.in_(account_ids),
Document.document_date >= date_from_obj,
Document.document_date <= date_to_obj,
)
)
# 🆕 فیلتر پروژه
if project_id:
turnover_query = turnover_query.filter(Document.project_id == project_id)
turnover_query = turnover_query.group_by(DocumentLine.account_id)
# ادامه کد...
2. بهروزرسانی Endpoint:
# hesabixAPI/adapters/api/v1/documents.py (خط 2335)
@router.post("/businesses/{business_id}/reports/pnl-period")
async def pnl_period_report_endpoint(
request: Request,
business_id: int,
body: Dict[str, Any] = Body(default={}),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
):
# استخراج پارامترها
fiscal_year_id = body.get('fiscal_year_id')
currency_id = body.get('currency_id')
date_from = body.get('date_from')
date_to = body.get('date_to')
project_id = body.get('project_id') # 🆕
# تبدیل نوع
if project_id is not None:
try:
project_id = int(project_id)
except (ValueError, TypeError):
project_id = None
# فراخوانی سرویس
result = get_pnl_period_report(
db=db,
business_id=business_id,
fiscal_year_id=fiscal_year_id,
currency_id=currency_id,
date_from=date_from,
date_to=date_to,
project_id=project_id, # 🆕
skip=skip,
take=take,
)
return success_response(data=result, message="گزارش سود و زیان دریافت شد")
Frontend
بهروزرسانی صفحه:
// lib/pages/business/pnl_period_report_page.dart
class _PnlPeriodReportPageState extends State<PnlPeriodReportPage> {
DateTime? _fromDate;
DateTime? _toDate;
int? _selectedFiscalYearId;
int? _selectedProjectId; // 🆕
List<Map<String, dynamic>> _fiscalYears = [];
Map<String, dynamic>? _reportData;
bool _loading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
children: [
const Text('گزارش سود و زیان دورهای'),
// 🆕 نمایش نام پروژه در عنوان
if (_selectedProjectId != null && _projectName != null)
Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const Icon(Icons.folder_special, size: 16),
const SizedBox(width: 4),
Text('پروژه: $_projectName', style: const TextStyle(fontSize: 14)),
],
),
),
],
),
actions: [
IconButton(
onPressed: _exportToPdf,
icon: const Icon(Icons.picture_as_pdf),
tooltip: 'خروجی PDF',
),
IconButton(
onPressed: _exportToExcel,
icon: const Icon(Icons.table_chart),
tooltip: 'خروجی Excel',
),
],
),
body: Column(
children: [
// 🆕 فیلترهای یکپارچه
CommonReportFilters(
businessId: widget.businessId,
apiClient: widget.apiClient,
calendarController: widget.calendarController,
fromDate: _fromDate,
toDate: _toDate,
onFromDateChanged: (date) {
setState(() => _fromDate = date);
_loadReport();
},
onToDateChanged: (date) {
setState(() => _toDate = date);
_loadReport();
},
onClearDates: () {
setState(() {
_fromDate = null;
_toDate = null;
});
_loadReport();
},
selectedFiscalYearId: _selectedFiscalYearId,
fiscalYears: _fiscalYears,
onFiscalYearChanged: (fyId) {
setState(() => _selectedFiscalYearId = fyId);
_loadReport();
},
selectedProjectId: _selectedProjectId,
onProjectChanged: (projectId) {
setState(() => _selectedProjectId = projectId);
_loadReport();
},
),
const SizedBox(height: 16),
// محتوای گزارش
Expanded(
child: _loading
? const Center(child: CircularProgressIndicator())
: _buildReportContent(),
),
],
),
);
}
Future<void> _loadReport() async {
if (_loading) return;
setState(() => _loading = true);
try {
final body = <String, dynamic>{};
if (_fromDate != null) body['date_from'] = _fromDate!.toIso8601String();
if (_toDate != null) body['date_to'] = _toDate!.toIso8601String();
if (_selectedFiscalYearId != null) body['fiscal_year_id'] = _selectedFiscalYearId;
if (_selectedProjectId != null) body['project_id'] = _selectedProjectId; // 🆕
final response = await widget.apiClient.post(
'/api/v1/businesses/${widget.businessId}/reports/pnl-period',
data: body,
);
if (response['success'] == true) {
setState(() {
_reportData = response['data'] as Map<String, dynamic>;
_loading = false;
});
}
} catch (e) {
setState(() => _loading = false);
// نمایش خطا
}
}
Widget _buildReportContent() {
if (_reportData == null) {
return const Center(child: Text('فیلترها را انتخاب کنید'));
}
final summary = _reportData!['summary'] as Map<String, dynamic>;
final revenue = summary['total_revenue'] as num? ?? 0;
final expense = summary['total_expense'] as num? ?? 0;
final netProfit = summary['net_profit_loss'] as num? ?? 0;
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 🆕 Card خلاصه با نمایش نام پروژه
Card(
color: Colors.blue.shade50,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
if (_selectedProjectId != null)
Row(
children: [
const Icon(Icons.folder_special, color: Colors.blue),
const SizedBox(width: 8),
Text(
'گزارش پروژه: $_projectName',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildSummaryItem('درآمد', revenue, Colors.green),
_buildSummaryItem('هزینه', expense, Colors.red),
_buildSummaryItem(
netProfit >= 0 ? 'سود' : 'زیان',
netProfit.abs(),
netProfit >= 0 ? Colors.green : Colors.red,
),
],
),
],
),
),
),
const SizedBox(height: 16),
// جداول درآمد و هزینه
_buildRevenueTable(),
const SizedBox(height: 16),
_buildExpenseTable(),
],
),
);
}
}
📊 نمایش بصری تغییرات
قبل از یکپارچهسازی:
┌─────────────────────────────────────────────────────┐
│ گزارش سود و زیان │
├─────────────────────────────────────────────────────┤
│ از تاریخ: [1404/01/01] تا تاریخ: [1404/03/31] │
│ سال مالی: [1404 ▼] │
├─────────────────────────────────────────────────────┤
│ درآمد: 500,000,000 | هزینه: 350,000,000 │
│ سود خالص: 150,000,000 │
├─────────────────────────────────────────────────────┤
│ [جدول تفصیلی درآمدها و هزینهها] │
└─────────────────────────────────────────────────────┘
بعد از یکپارچهسازی:
┌─────────────────────────────────────────────────────┐
│ گزارش سود و زیان 📁 پروژه: ساخت ساختمان A │
├─────────────────────────────────────────────────────┤
│ از تاریخ: [1404/01/01] تا تاریخ: [1404/03/31] │
│ سال مالی: [1404 ▼] │
│ 🆕 پروژه: [ساخت ساختمان A ▼] │
├─────────────────────────────────────────────────────┤
│ 📊 عملکرد پروژه در دوره انتخابی: │
│ درآمد: 200,000,000 | هزینه: 150,000,000 │
│ سود خالص: 50,000,000 | حاشیه سود: 25% │
├─────────────────────────────────────────────────────┤
│ [جدول تفصیلی - فقط تراکنشهای این پروژه] │
└─────────────────────────────────────────────────────┘
🎯 ویژگیهای خاص هر گزارش
1. گزارش دفتر کل
کاربرد پروژه:
- مشاهده تراکنشهای یک حساب در پروژه خاص
- مانده حساب دریافتنی/پرداختنی در پروژه
مثال query:
if project_id:
query = query.filter(Document.project_id == project_id)
2. گزارش سود و زیان
کاربرد پروژه:
- محاسبه سودآوری هر پروژه
- مقایسه عملکرد پروژهها
- تحلیل حاشیه سود
امکان جدید: مقایسه چند پروژه
// انتخاب چند پروژه برای مقایسه
List<int> _selectedProjects = [];
// نمایش نمودار مقایسهای
BarChart(
data: _selectedProjects.map((projectId) {
return {
'project': getProjectName(projectId),
'profit': getProjectProfit(projectId),
};
}),
)
3. گزارش روزنامه
کاربرد پروژه:
- مشاهده همه اسناد ثبت شده در پروژه
- ممیزی تراکنشهای پروژه
4. گزارش تراز
کاربرد پروژه:
- تراز حسابها محدود به پروژه خاص
- بررسی صحت ثبتهای پروژه
5. گزارشهای فروش
کاربرد پروژه:
- فروش روزانه/ماهانه هر پروژه
- محصولات پرفروش در پروژه
- مشتریان کلیدی پروژه
🎨 UI/UX پیشرفته
1. نمایش Badge پروژه
// در هدر گزارش
if (_selectedProjectId != null)
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.folder_special, size: 16, color: Colors.blue),
SizedBox(width: 4),
Text('پروژه: $_projectName'),
IconButton(
icon: Icon(Icons.close, size: 16),
onPressed: () {
setState(() => _selectedProjectId = null);
_loadReport();
},
),
],
),
),
2. Export با نام پروژه
Future<void> _exportToExcel() async {
String filename = 'گزارش_سود_و_زیان';
// 🆕 اضافه کردن نام پروژه به نام فایل
if (_selectedProjectId != null && _projectName != null) {
filename += '_${_projectName.replaceAll(' ', '_')}';
}
filename += '_${_fromDate}_${_toDate}.xlsx';
// ادامه export...
}
3. ذخیره فیلترهای پیشفرض
// ذخیره آخرین فیلتر انتخابی
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setInt('last_selected_project_id', _selectedProjectId ?? 0);
// بازیابی در بارگذاری
_selectedProjectId = prefs.getInt('last_selected_project_id');
🔍 کیسهای خاص
کیس 1: گزارش بدون پروژه
// اضافه کردن گزینه "فقط اسناد بدون پروژه"
DropdownMenuItem(
value: -1, // مقدار خاص
child: Text('اسناد بدون پروژه'),
)
// در API
if project_id == -1:
query = query.filter(Document.project_id == None)
کیس 2: مقایسه با/بدون پروژه
// Toggle برای نمایش همزمان
bool _showComparison = false;
if (_showComparison) {
// نمایش 2 ستون: با پروژه / بدون پروژه
Row(
children: [
Expanded(child: _buildProjectReport()),
Expanded(child: _buildNonProjectReport()),
],
)
}
کیس 3: گزارش چند پروژه
// انتخاب چندتایی برای مقایسه
List<int> _selectedProjects = [];
MultiProjectSelector(
selectedProjects: _selectedProjects,
onChanged: (projects) {
setState(() => _selectedProjects = projects);
_loadComparativeReport();
},
)
🚀 نقشه راه پیادهسازی (Roadmap)
Week 1: گزارشات حسابداری اصلی
- دفتر کل (2 ساعت)
- دفتر روزنامه (2 ساعت)
- تراز آزمایشی (1.5 ساعت)
- سود و زیان دورهای (2 ساعت)
- سود و زیان تجمعی (2 ساعت)
- تست و اصلاح باگ (2 ساعت)
جمع: 11.5 ساعت
Week 2: گزارشات اشخاص و فروش
- تراکنشهای اشخاص (1.5 ساعت)
- بدهکاران/بستانکاران (2 ساعت)
- فروش روزانه (1.5 ساعت)
- خرید روزانه (1.5 ساعت)
- فروش ماهانه (1 ساعت)
- تست کلی (2 ساعت)
جمع: 9.5 ساعت
Week 3: گزارشات بانکی و انبار
- گردش بانک (2 ساعت)
- گردش صندوق (1.5 ساعت)
- کاردکس (1.5 ساعت)
- سایر گزارشات (2 ساعت)
- تست نهایی (2 ساعت)
جمع: 9 ساعت
⏱️ زمان کل تخمینی: 30 ساعت (حدود 4 روز کاری)
📝 الگوی کد برای کپی-پیست
الگوی Backend (Service)
def get_YOUR_report(
db: Session,
business_id: int,
# ... پارامترهای موجود
project_id: Optional[int] = None, # 🆕 همیشه اضافه کنید
skip: int = 0,
take: int = 100,
) -> Dict[str, Any]:
"""گزارش با فیلتر پروژه"""
# Query پایه
query = db.query(...).join(Document).filter(
Document.business_id == business_id,
# ... سایر فیلترها
)
# 🆕 فیلتر پروژه - همیشه همین ساختار
if project_id:
query = query.filter(Document.project_id == project_id)
# ادامه query...
الگوی Backend (API Endpoint)
@router.post("/businesses/{business_id}/reports/YOUR-REPORT")
async def your_report_endpoint(
request: Request,
business_id: int,
body: Dict[str, Any] = Body(default={}),
# ...
):
# استخراج پارامترها
project_id = body.get('project_id') # 🆕 اضافه کنید
# تبدیل نوع
if project_id is not None:
try:
project_id = int(project_id)
except (ValueError, TypeError):
project_id = None
# فراخوانی سرویس
result = get_YOUR_report(
# ... سایر پارامترها
project_id=project_id, # 🆕 پاس دهید
)
الگوی Frontend (Page State)
class _YourReportPageState extends State<YourReportPage> {
// ... سایر state ها
int? _selectedProjectId; // 🆕 همیشه اضافه کنید
String? _projectName; // 🆕 برای نمایش
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
children: [
Text('عنوان گزارش'),
// 🆕 نمایش badge پروژه
if (_selectedProjectId != null)
_buildProjectBadge(),
],
),
),
body: Column(
children: [
// 🆕 فیلترهای یکپارچه
CommonReportFilters(
businessId: widget.businessId,
apiClient: widget.apiClient,
calendarController: widget.calendarController,
// ... سایر فیلترها
selectedProjectId: _selectedProjectId,
onProjectChanged: _onProjectChanged,
),
Expanded(child: _buildContent()),
],
),
);
}
void _onProjectChanged(int? projectId) {
setState(() => _selectedProjectId = projectId);
if (projectId != null) {
_loadProjectName(projectId);
} else {
_projectName = null;
}
_loadReport();
}
}
🧪 نمونههای تست
تست Backend
# test_project_filter_in_reports.py
def test_general_ledger_with_project():
"""تست فیلتر پروژه در دفتر کل"""
# ایجاد 2 پروژه
project1 = create_project(db, business_id=1, data={'code': 'P1', 'name': 'پروژه 1'})
project2 = create_project(db, business_id=1, data={'code': 'P2', 'name': 'پروژه 2'})
# ایجاد 4 فاکتور: 2 برای پروژه 1، 2 برای پروژه 2
invoice1 = create_invoice(db, business_id=1, data={'project_id': project1.id, ...})
invoice2 = create_invoice(db, business_id=1, data={'project_id': project1.id, ...})
invoice3 = create_invoice(db, business_id=1, data={'project_id': project2.id, ...})
invoice4 = create_invoice(db, business_id=1, data={'project_id': project2.id, ...})
# گزارش بدون فیلتر پروژه - باید 4 سند نمایش دهد
report_all = get_general_ledger_report(db, business_id=1, account_ids=[...])
assert report_all['pagination']['total'] == 4
# گزارش با فیلتر پروژه 1 - باید 2 سند نمایش دهد
report_p1 = get_general_ledger_report(db, business_id=1, account_ids=[...], project_id=project1.id)
assert report_p1['pagination']['total'] == 2
# گزارش با فیلتر پروژه 2 - باید 2 سند نمایش دهد
report_p2 = get_general_ledger_report(db, business_id=1, account_ids=[...], project_id=project2.id)
assert report_p2['pagination']['total'] == 2
تست Frontend
// test/reports/general_ledger_project_filter_test.dart
testWidgets('General Ledger with project filter', (tester) async {
await tester.pumpWidget(MyApp());
// رفتن به صفحه گزارش
await tester.tap(find.text('گزارش دفتر کل'));
await tester.pumpAndSettle();
// انتخاب پروژه
await tester.tap(find.byType(ProjectSelectorWidget));
await tester.pumpAndSettle();
await tester.tap(find.text('پروژه A'));
await tester.pumpAndSettle();
// بررسی نمایش badge
expect(find.text('پروژه: پروژه A'), findsOneWidget);
// بررسی فیلتر شدن دادهها
// ...
});
📈 KPIs و متریکها
متریکهای قابل اندازهگیری برای هر پروژه:
-
مالی:
- درآمد کل
- هزینه کل
- سود/زیان خالص
- حاشیه سود (Profit Margin)
- بازگشت سرمایه (ROI)
-
عملیاتی:
- تعداد اسناد
- تعداد فاکتورها
- تعداد تراکنشها
- میانگین ارزش فاکتور
-
زمانی:
- مدت زمان پروژه
- درصد پیشرفت زمانی
- پیشبینی تاریخ اتمام
-
بودجه:
- بودجه تخصیصی
- هزینه واقعی
- انحراف از بودجه (Budget Variance)
- درصد مصرف بودجه
Dashboard پروژه (پیشنهاد آینده)
ProjectDashboard(
projectId: _selectedProjectId,
widgets: [
// KPI Cards
KPICard(title: 'درآمد', value: revenue, icon: Icons.trending_up),
KPICard(title: 'هزینه', value: expense, icon: Icons.trending_down),
KPICard(title: 'سود', value: profit, icon: Icons.monetization_on),
// نمودارها
ProfitTrendChart(projectId: _selectedProjectId),
ExpenseBreakdownPieChart(projectId: _selectedProjectId),
BudgetVsActualChart(projectId: _selectedProjectId),
],
)
✅ چکلیست تکمیل
Backend (11 فایل)
general_ledger_service.py+ endpointjournal_ledger_service.py+ endpointtrial_balance_service.py+ endpointpnl_service.py(2 تابع) + endpoints- گزارشات فروش (3 سرویس)
- گزارشات بانک (2 سرویس)
- تستهای واحد
Frontend (11 فایل)
common_report_filters.dart(widget جدید)general_ledger_report_page.dartjournal_ledger_report_page.darttrial_balance_report_page.dartpnl_period_report_page.dartpnl_cumulative_report_page.dartpeople_transactions_report_page.dartdaily_sales_report_page.dartbank_accounts_turnover_report_page.dart- تستهای UI
Documentation
- سناریوی کلی
- راهنمای یکپارچهسازی
- نمونههای API (Swagger)
- راهنمای کاربری
💰 تخمین هزینه/زمان
| مرحله | تخمین زمان | توسعهدهنده |
|---|---|---|
| Backend Services | 12 ساعت | Backend Dev |
| Backend Endpoints | 6 ساعت | Backend Dev |
| Frontend Widgets | 4 ساعت | Frontend Dev |
| Frontend Pages | 12 ساعت | Frontend Dev |
| Testing | 8 ساعت | QA Team |
| Documentation | 4 ساعت | Tech Writer |
| جمع | 46 ساعت | ~6 روز کاری |
🎁 ارزش افزوده برای کاربران
قبل از پیادهسازی:
❌ گزارشها کلی و نامشخص
❌ عدم امکان تحلیل عملکرد پروژه
❌ سختی در پیگیری هزینههای پروژه
❌ نیاز به Export و تحلیل دستی در Excel
بعد از پیادهسازی:
✅ گزارش دقیق هر پروژه
✅ تحلیل سودآوری لحظهای
✅ کنترل بودجه پروژه
✅ تصمیمگیری مبتنی بر داده
✅ صرفهجویی ساعتها زمان
🏆 Best Practices
- Performance: Cache کردن لیست پروژهها (TTL: 5 دقیقه)
- UX: نمایش واضح "بدون فیلتر" vs "همه پروژهها"
- Export: اضافه کردن نام پروژه به فایلهای خروجی
- Validation: بررسی تعلق پروژه به همان کسبوکار
- Logging: ثبت log برای تحلیل استفاده از فیلتر پروژه
نسخه: 1.0.0
تاریخ: دسامبر 2025
وضعیت: ✅ آماده شروع پیادهسازی