Compare commits

..

82 commits
test ... master

Author SHA1 Message Date
Hesabix 805abe8f51 bug fix in updater 2025-08-24 14:45:56 +00:00
Hesabix 189bf9fbdd progress in bank card 2025-08-24 14:38:06 +00:00
Hesabix ee5d644358 Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-24 10:37:12 +00:00
Hesabix 968811507e add button to convert preinvoice to invoice 2025-08-24 10:37:09 +00:00
Gloomy d877be1bb8 Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-24 07:42:29 +00:00
Gloomy fb71c35ac3 add routes documentation for some plugins 2025-08-24 07:40:17 +00:00
Hesabix c8c2bb11d0 start working on payment ID 2025-08-24 07:37:10 +00:00
Hesabix b7ecafb3a7 bug fix in sell invoice pdf export for via shortlinks 2025-08-24 07:00:24 +00:00
Hesabix 5159b0fcda Update hesabixCore/src/Controller/SellController.php 2025-08-23 23:06:07 +03:30
Hesabix 5334b1fddb more progress in hrm plugin 2025-08-23 15:47:57 +00:00
Hesabix 1418591120 Resolve merge conflicts in BusinessController.php 2025-08-22 16:21:50 +00:00
Hesabix b94ae7733e bug fix in approval system and syart working on hrm plugin 2025-08-22 16:18:44 +00:00
Gloomy 043caee783 update for ImportWorkflow plugin 2025-08-22 10:41:33 +00:00
Gloomy 56bd4978e9 update for Warranty plugin 2025-08-22 10:20:14 +00:00
Gloomy c275206cae update for ImportWorkflow plugin 2025-08-22 09:39:05 +00:00
Gloomy 681262c33e update for Warranty plugin 2025-08-22 09:08:47 +00:00
Gloomy 6cbd431edb update for Warranty plugin & barcode scanner 2025-08-21 23:00:28 +00:00
Gloomy 35add500ca update for Warranty plugin 2025-08-21 22:33:44 +00:00
Gloomy 2d6919c660 update Warranty plugin / add storeroom ticket changes 2025-08-21 22:23:17 +00:00
Hesabix 2e4b0a68f2 more improve in sell report 2025-08-21 21:49:49 +00:00
Hesabix fa46e410fc add sell report 2025-08-21 21:09:26 +00:00
Hesabix f609c4176f bug fix in open balance 2025-08-21 15:29:02 +00:00
Gloomy da074d2e89 Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-20 20:07:11 +00:00
Gloomy 91d2558893 update Warranty & ImportWorkflow plugins 2025-08-20 20:07:02 +00:00
Hesabix 11865d453d remove unused controller 2025-08-20 18:59:35 +00:00
Hesabix 79b887041e more progress in changes with new columns in hesabdariDoc 2025-08-20 18:00:04 +00:00
Hesabix ef17ba4d78 more bug fix in controllers 2025-08-20 17:32:36 +00:00
Hesabix 2dde89e03c Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-20 16:56:17 +00:00
Hesabix 0f954ba6a1 bug fix in hesabdariDoc and buy list invoices 2025-08-20 16:56:14 +00:00
Gloomy 28ad56d972 update for two-step system 2025-08-20 08:11:52 +00:00
Hesabix 8d91bcd4ea add more filter in hesabdari controller 2025-08-20 00:13:11 +00:00
Hesabix 9af86b989b almost finish buy system with new changes in hesabdariDoc 2025-08-19 23:50:52 +00:00
Hesabix 45c03051a0 Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-19 21:21:00 +00:00
Hesabix b1b0e4b00d basic business change with add with new colums 2025-08-19 21:20:58 +00:00
Gloomy 65c6f38ef3 update for Warranty plugin 2025-08-19 20:51:48 +00:00
Gloomy 68bd621a58 update two-step system 2025-08-19 20:47:11 +00:00
Gloomy f137fcb0dc remove approve button for payments 2025-08-19 18:50:07 +00:00
Hesabix 3a6558ae64 edit report controller with new changes on hesabdariDoc 2025-08-19 18:43:54 +00:00
Gloomy e6c94ae509 Implement conditional rendering for approval status and action based on two-step approval requirement 2025-08-19 18:35:29 +00:00
Hesabix 11cc70424d check promps for edit persons part with new changes in hesabdariDoc 2025-08-19 18:26:18 +00:00
Hesabix 77e0c5a975 add migration for fill colums for is approved and priview in hesabdariDoc and storeroomTicket 2025-08-19 17:52:46 +00:00
Hesabix 8ff15b1e67 Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-19 17:27:00 +00:00
Hesabix 3d6f27ef80 redesign some pages of store components 2025-08-19 17:26:56 +00:00
Gloomy c841489fe4 update two-step system 2025-08-19 17:19:59 +00:00
Gloomy 758111de76 update for Moadian plugin 2025-08-19 16:37:49 +00:00
Gloomy f3517d55d6 update for two-step system 2025-08-19 14:16:17 +00:00
Gloomy e775de8f77 update for Moadian plugin 2025-08-19 13:50:11 +00:00
Gloomy ac49a0229e update for two-step approval 2025-08-19 00:41:39 +00:00
Gloomy ff89d596b7 Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-19 00:35:00 +00:00
Gloomy 8d11485530 fix some bugs 2025-08-19 00:34:56 +00:00
Hesabix 19d4a967cb Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-18 21:55:30 +00:00
Hesabix fb0a2482e9 add icons for two plugins 2025-08-18 21:53:42 +00:00
Gloomy 15d2f40e5d update two-step ui 2025-08-18 21:09:07 +00:00
Gloomy 484c7a0a64 update for Moadian plugin 2025-08-18 20:33:09 +00:00
Gloomy dd78e12a7a update two-step ui/fix Warranty plugin bugs 2025-08-18 19:53:48 +00:00
Gloomy d3bd560e36 update for ImportWorkflow plugin 2025-08-18 19:10:21 +00:00
Gloomy aee56d5548 Add Warranty & ImportWorkflow plugins/ add two-step approval for docs in accpro 2025-08-18 18:50:25 +00:00
Gloomy ded4cff458 bug fix 2025-08-18 18:40:42 +00:00
Gloomy d231e81252 bug fix 2025-08-18 18:40:42 +00:00
Gloomy c09fe66a5f beta version 2025-08-18 18:40:42 +00:00
Hesabix 50ca4045bc add seltment to person card 2025-08-18 07:33:23 +00:00
Hesabix a97a29d50e improve business info from admin panel 2025-08-17 21:21:38 +00:00
Hesabix ca043a913f add some widgets about cheque to dashboard 2025-08-17 17:34:52 +00:00
Hesabix 93bdf0fac4 increase timeout of apache and php in installation script 2025-08-17 15:39:47 +00:00
Hesabix 6fad9552ad increase time put of update process 2025-08-17 11:57:46 +00:00
Hesabix e40074cd59 bug fix in sell doc export pdf 2025-08-17 11:44:36 +00:00
Hesabix 2f144c0d9d bug fix in automatic update 2025-08-17 11:26:24 +00:00
Hesabix 789618927d improve person inport excell 2025-08-16 13:58:19 +00:00
Hesabix 33cf15dedc bug fix in import commodity 2025-08-16 12:53:17 +00:00
Hesabix 09fbe891e6 bug fix in deep in new version of vue js 2025-08-16 12:18:07 +00:00
Hesabix fbf9a836a8 perpare fox realase 2025-08-16 12:13:39 +00:00
Hesabix da644e3260 add first release for oauth application syncing 2025-08-16 01:56:06 +00:00
Hesabix 51d68b9874 first release of custome invoice designer 2025-08-15 22:19:00 +00:00
Hesabix 29625b7afa add guide for custome invoice 2025-08-15 16:31:58 +00:00
Hesabix 1bc05834f9 bug fix in backup export in excell 2025-08-15 08:58:16 +00:00
Hesabix d87d3ba137 add date filter to explore account report 2025-08-14 14:13:47 +00:00
Hesabix b1ce11930e bug fix in user check login and jump to login page 2025-08-14 10:04:43 +00:00
Hesabix 56964c96b7 improve general setting page 2025-08-13 16:14:16 +00:00
Hesabix 251ebe59f7 add backup in excell in accpro plugin 2025-08-13 15:04:04 +00:00
Hesabix 290b272872 bug fix in persons s/r 2025-08-13 00:58:38 +00:00
Hesabix 63472c1d13 add sort to person send and revive 2025-08-13 00:20:37 +00:00
Hesabix 5afd0236c6 progress in close year 2025-08-12 21:58:06 +00:00
203 changed files with 40478 additions and 6205 deletions

View file

@ -0,0 +1,189 @@
# خلاصه پاک‌سازی OAuth Backend
## 🧹 تغییرات انجام شده
### 1. حذف فایل‌های غیرضروری
#### حذف شده:
- `hesabixCore/templates/oauth/authorize.html.twig` - صفحه Twig قدیمی
- `hesabixCore/templates/oauth/error.html.twig` - صفحه خطای Twig
- `hesabixCore/templates/oauth/` - پوشه کامل templates
#### دلیل حذف:
- صفحه authorization حالا در frontend پیاده‌سازی شده
- نیازی به template های Twig نیست
### 2. تمیزسازی OAuthController
#### حذف شده:
- **Method تکراری:** `authorizeApiOld()` - نسخه قدیمی
- **کد غیرضروری:** بخش‌های مربوط به render کردن template ها
- **Method های اضافی:** کدهای تکراری و غیرضروری
#### بهبود شده:
- **Error Handling:** به جای render کردن template، JSON response برمی‌گرداند
- **Code Structure:** کد تمیزتر و قابل خواندن‌تر
- **Performance:** حذف کدهای غیرضروری
### 3. ساختار نهایی OAuthController
#### Endpoints موجود:
```php
// 1. Authorization endpoint (هدایت به frontend)
#[Route('/authorize', name: 'oauth_authorize', methods: ['GET'])]
public function authorize(Request $request): Response
// 2. API endpoint برای frontend
#[Route('/api/oauth/authorize', name: 'api_oauth_authorize', methods: ['POST'])]
public function authorizeApi(Request $request): JsonResponse
// 3. Token endpoint
#[Route('/token', name: 'oauth_token', methods: ['POST'])]
public function token(Request $request): JsonResponse
// 4. User Info endpoint
#[Route('/userinfo', name: 'oauth_userinfo', methods: ['GET'])]
public function userinfo(Request $request): JsonResponse
// 5. Revoke endpoint
#[Route('/revoke', name: 'oauth_revoke', methods: ['POST'])]
public function revoke(Request $request): JsonResponse
// 6. Discovery endpoint
#[Route('/.well-known/oauth-authorization-server', name: 'oauth_discovery', methods: ['GET'])]
public function discovery(): JsonResponse
```
## 📊 مقایسه قبل و بعد
### قبل از پاک‌سازی:
```
hesabixCore/
├── templates/
│ └── oauth/
│ ├── authorize.html.twig (7.8KB)
│ └── error.html.twig (3.1KB)
└── src/Controller/
└── OAuthController.php (442 خط)
```
### بعد از پاک‌سازی:
```
hesabixCore/
└── src/Controller/
└── OAuthController.php (280 خط)
```
### کاهش حجم:
- **حذف شده:** 10.9KB از template files
- **کاهش خطوط کد:** 162 خط (37% کاهش)
- **حذف پوشه:** `templates/oauth/`
## ✅ مزایای پاک‌سازی
### 🚀 عملکرد بهتر
- کاهش حجم کد
- حذف کدهای تکراری
- بهبود سرعت بارگذاری
### 🧹 نگهداری آسان‌تر
- کد تمیزتر و قابل خواندن
- حذف وابستگی‌های غیرضروری
- ساختار ساده‌تر
### 🔒 امنیت بیشتر
- حذف کدهای قدیمی که ممکن است آسیب‌پذیر باشند
- تمرکز روی endpoint های ضروری
- Error handling بهتر
### 📱 سازگاری کامل با Frontend
- تمام منطق UI در frontend
- Backend فقط API endpoints
- جداسازی مسئولیت‌ها
## 🔧 نکات فنی
### Error Handling جدید:
```php
// قبل
return $this->render('oauth/error.html.twig', [
'error' => $e->getMessage()
]);
// بعد
return new JsonResponse([
'error' => 'invalid_request',
'error_description' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
```
### User Info ساده‌تر:
```php
// قبل: بررسی دستی Authorization header
$authorization = $request->headers->get('Authorization');
$token = substr($authorization, 7);
$accessToken = $this->oauthService->validateAccessToken($token);
// بعد: استفاده از Symfony Security
$user = $this->getUser();
if (!$user) {
return $this->json(['error' => 'invalid_token'], 401);
}
```
## 🧪 تست سیستم
### تست Authorization Flow:
```bash
# 1. درخواست مجوز
curl "https://your-domain.com/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://your-app.com/callback&response_type=code&scope=read_profile&state=test123"
# 2. باید به frontend هدایت شود
# https://your-domain.com/u/oauth/authorize?client_id=...
```
### تست API Endpoint:
```bash
# تایید مجوز
curl -X POST "https://your-domain.com/api/oauth/authorize" \
-H "Content-Type: application/json" \
-H "X-AUTH-TOKEN: YOUR_TOKEN" \
-d '{
"client_id": "YOUR_CLIENT_ID",
"redirect_uri": "https://your-app.com/callback",
"scope": "read_profile",
"state": "test123",
"approved": true
}'
```
## 📋 چک‌لیست پاک‌سازی
### ✅ انجام شده:
- [x] حذف template files
- [x] حذف method های تکراری
- [x] تمیزسازی OAuthController
- [x] بهبود error handling
- [x] پاک کردن cache
- [x] تست عملکرد
### 🔄 بررسی نهایی:
- [ ] تست کامل OAuth flow
- [ ] بررسی performance
- [ ] تست error scenarios
- [ ] بررسی security
## 🎯 نتیجه نهایی
سیستم OAuth حالا:
- **ساده‌تر** و قابل نگهداری‌تر است
- **سریع‌تر** و کارآمدتر است
- **امن‌تر** و قابل اعتمادتر است
- **سازگار** با frontend است
---
**تاریخ پاک‌سازی:** 2025-08-16
**وضعیت:** تکمیل ✅
**توسعه‌دهنده:** Hesabix Team

View file

@ -0,0 +1,957 @@
# مستندات کامل سیستم OAuth 2.0 - Hesabix
## 📋 فهرست مطالب
1. [معرفی OAuth 2.0](#معرفی-oauth-20)
2. [معماری سیستم](#معماری-سیستم)
3. [بخش مدیریت](#بخش-مدیریت)
4. [بخش کاربری](#بخش-کاربری)
5. [API Documentation](#api-documentation)
6. [نحوه اتصال](#نحوه-اتصال)
7. [امنیت](#امنیت)
8. [مثال‌های عملی](#مثال‌های-عملی)
9. [عیب‌یابی](#عیب‌یابی)
10. [پشتیبانی](#پشتیبانی)
---
## 🚀 معرفی OAuth 2.0
OAuth 2.0 یک پروتکل استاندارد برای احراز هویت و مجوزدهی است که به برنامه‌های خارجی اجازه می‌دهد بدون نیاز به رمز عبور، به حساب کاربران دسترسی داشته باشند.
### مزایای OAuth 2.0:
- ✅ **امنیت بالا:** عدم اشتراک‌گذاری رمز عبور
- ✅ **کنترل دسترسی:** محدود کردن دسترسی‌ها با Scope
- ✅ **قابلیت لغو:** امکان لغو دسترسی در هر زمان
- ✅ **استاندارد:** سازگار با پروتکل‌های جهانی
- ✅ **IP Whitelist:** کنترل دسترسی بر اساس IP
- ✅ **Rate Limiting:** محدودیت تعداد درخواست
---
## 🏗️ معماری سیستم
### اجزای اصلی:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Client App │ │ OAuth Server │ │ Resource Owner │
│ (Third Party) │◄──►│ (Hesabix) │◄──►│ (User) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
### جریان OAuth:
1. **Client Registration:** ثبت برنامه در بخش مدیریت
2. **Authorization Request:** درخواست مجوز از کاربر
3. **User Consent:** تأیید کاربر
4. **Authorization Code:** دریافت کد مجوز
5. **Token Exchange:** تبدیل کد به Access Token
6. **Resource Access:** دسترسی به منابع
### پایگاه داده:
```sql
-- جداول OAuth
oauth_application -- برنامه‌های ثبت شده
oauth_scope -- محدوده‌های دسترسی
oauth_authorization_code -- کدهای مجوز موقت
oauth_access_token -- توکن‌های دسترسی
```
---
## ⚙️ بخش مدیریت
### 1. دسترسی به بخش مدیریت
```
مدیریت سیستم → تنظیمات سیستم → تب "برنامه‌های OAuth"
```
### 2. آمار کلی
سیستم چهار کارت آمار نمایش می‌دهد:
- **کل برنامه‌ها:** تعداد کل برنامه‌های ثبت شده
- **فعال:** تعداد برنامه‌های فعال
- **غیرفعال:** تعداد برنامه‌های غیرفعال
- **جدید (7 روز):** برنامه‌های ایجاد شده در هفته گذشته
### 3. ایجاد برنامه جدید
#### مراحل ایجاد:
1. **کلیک روی "ایجاد برنامه جدید"**
2. **پر کردن فرم:**
- **نام برنامه:** نام منحصر به فرد برنامه
- **توضیحات:** توضیح کاربرد برنامه
- **آدرس وب‌سایت:** URL اصلی برنامه
- **آدرس بازگشت:** URL callback برنامه
- **محدودیت درخواست:** تعداد درخواست مجاز در ساعت
#### تنظیمات امنیتی:
##### IP Whitelist:
```
- در صورت خالی بودن: از هر IP مجاز است
- افزودن IP: 192.168.1.1 یا 192.168.1.0/24
- پشتیبانی از CIDR notation
- Validation خودکار آدرس‌های IP
```
##### Scope Management:
```
read_profile - دسترسی به اطلاعات پروفایل
write_profile - تغییر اطلاعات پروفایل
read_business - دسترسی به اطلاعات کسب و کار
write_business - تغییر اطلاعات کسب و کار
read_financial - دسترسی به اطلاعات مالی
write_financial - تغییر اطلاعات مالی
read_contacts - دسترسی به لیست مخاطبین
write_contacts - تغییر لیست مخاطبین
read_documents - دسترسی به اسناد
write_documents - تغییر اسناد
admin_access - دسترسی مدیریتی
```
### 4. مدیریت برنامه‌ها
#### کارت برنامه:
- **وضعیت:** فعال/غیرفعال با رنگ‌بندی
- **Client ID:** شناسه یکتا برنامه
- **تاریخ ایجاد:** تاریخ ثبت برنامه
- **توضیحات:** شرح برنامه
#### عملیات موجود:
##### دکمه‌های اصلی:
- **ویرایش:** تغییر اطلاعات برنامه
- **آمار:** مشاهده آمار استفاده
- **فعال/غیرفعال:** تغییر وضعیت برنامه
##### منوی سه نقطه:
- **بازسازی کلید:** تولید Client Secret جدید
- **لغو توکن‌ها:** لغو تمام توکن‌های فعال
- **حذف:** حذف کامل برنامه
### 5. اطلاعات امنیتی
پس از ایجاد برنامه، اطلاعات زیر نمایش داده می‌شود:
```
Client ID: mL0qT1fkIL6MCJfxIPAh7nM2cQ7ykxEy
Client Secret: goM7latD9akY83z2O2e9IIEYED3Re6sRMd36f5cUSYHm389PPSqYbFHSX8GtQ9H1
```
⚠️ **هشدار:** این اطلاعات را در جای امنی ذخیره کنید!
---
## 👤 بخش کاربری
### 1. صفحه مجوزدهی
هنگام اتصال برنامه خارجی، کاربر به صفحه زیر هدایت می‌شود:
```
┌─────────────────────────────────────┐
│ مجوزدهی OAuth │
├─────────────────────────────────────┤
│ │
│ [آیکون برنامه] نام برنامه │
│ توضیحات برنامه... │
│ │
│ این برنامه درخواست دسترسی به: │
│ ✓ خواندن اطلاعات پروفایل │
│ ✓ خواندن اطلاعات کسب و کار │
│ │
│ [لغو] [تأیید] │
└─────────────────────────────────────┘
```
### 2. اطلاعات نمایش داده شده:
- **نام و لوگوی برنامه**
- **توضیحات برنامه**
- **محدوده‌های دسترسی درخواستی**
- **دکمه‌های تأیید/لغو**
### 3. تصمیم کاربر:
- **تأیید:** ادامه فرآیند OAuth
- **لغو:** بازگشت به برنامه اصلی
---
## 📡 API Documentation
### Base URL
```
https://your-domain.com/oauth
```
### 1. Authorization Endpoint
#### درخواست مجوز:
```http
GET /oauth/authorize
```
#### پارامترهای مورد نیاز:
```javascript
{
"response_type": "code",
"client_id": "mL0qT1fkIL6MCJfxIPAh7nM2cQ7ykxEy",
"redirect_uri": "https://your-app.com/callback",
"scope": "read_profile read_business",
"state": "random_string_for_csrf"
}
```
#### پاسخ موفق:
```http
HTTP/1.1 302 Found
Location: https://your-app.com/callback?code=AUTHORIZATION_CODE&state=random_string
```
### 2. Token Endpoint
#### درخواست Access Token:
```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
```
#### پارامترهای مورد نیاز:
```javascript
{
"grant_type": "authorization_code",
"client_id": "mL0qT1fkIL6MCJfxIPAh7nM2cQ7ykxEy",
"client_secret": "goM7latD9akY83z2O2e9IIEYED3Re6sRMd36f5cUSYHm389PPSqYbFHSX8GtQ9H1",
"code": "AUTHORIZATION_CODE",
"redirect_uri": "https://your-app.com/callback"
}
```
#### پاسخ موفق:
```json
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200...",
"scope": "read_profile read_business"
}
```
### 3. User Info Endpoint
#### درخواست اطلاعات کاربر:
```http
GET /oauth/userinfo
Authorization: Bearer ACCESS_TOKEN
```
#### پاسخ موفق:
```json
{
"id": 123,
"email": "user@example.com",
"name": "نام کاربر",
"profile": {
"phone": "+989123456789",
"address": "تهران، ایران"
}
}
```
### 4. Refresh Token Endpoint
#### تمدید Access Token:
```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
```
#### پارامترهای مورد نیاز:
```javascript
{
"grant_type": "refresh_token",
"client_id": "mL0qT1fkIL6MCJfxIPAh7nM2cQ7ykxEy",
"client_secret": "goM7latD9akY83z2O2e9IIEYED3Re6sRMd36f5cUSYHm389PPSqYbFHSX8GtQ9H1",
"refresh_token": "def50200..."
}
```
### 5. Revoke Endpoint
#### لغو Token:
```http
POST /oauth/revoke
Authorization: Bearer ACCESS_TOKEN
```
#### پارامترهای مورد نیاز:
```javascript
{
"token": "ACCESS_TOKEN_OR_REFRESH_TOKEN"
}
```
### 6. Discovery Endpoint
#### دریافت اطلاعات OAuth Server:
```http
GET /.well-known/oauth-authorization-server
```
#### پاسخ:
```json
{
"issuer": "https://hesabix.ir",
"authorization_endpoint": "https://hesabix.ir/oauth/authorize",
"token_endpoint": "https://hesabix.ir/oauth/token",
"userinfo_endpoint": "https://hesabix.ir/oauth/userinfo",
"revocation_endpoint": "https://hesabix.ir/oauth/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_post"],
"scopes_supported": [
"read_profile",
"write_profile",
"read_business",
"write_business",
"read_financial",
"write_financial",
"read_contacts",
"write_contacts",
"read_documents",
"write_documents",
"admin_access"
]
}
```
---
## 🔗 نحوه اتصال
### 1. ثبت برنامه
ابتدا برنامه خود را در بخش مدیریت ثبت کنید:
```javascript
// اطلاعات مورد نیاز
const appInfo = {
name: "My Application",
description: "توضیح برنامه من",
website: "https://myapp.com",
redirectUri: "https://myapp.com/oauth/callback",
allowedScopes: ["read_profile", "read_business"],
ipWhitelist: ["192.168.1.0/24"], // اختیاری
rateLimit: 1000
};
```
### 2. پیاده‌سازی OAuth Flow
#### مرحله 1: درخواست مجوز
```javascript
function initiateOAuth() {
const params = new URLSearchParams({
response_type: 'code',
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'https://myapp.com/oauth/callback',
scope: 'read_profile read_business',
state: generateRandomString()
});
window.location.href = `https://hesabix.com/oauth/authorize?${params}`;
}
```
#### مرحله 2: دریافت Authorization Code
```javascript
// در callback URL
function handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
if (code && state) {
exchangeCodeForToken(code);
}
}
```
#### مرحله 3: تبدیل Code به Token
```javascript
async function exchangeCodeForToken(code) {
const response = await fetch('https://hesabix.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
code: code,
redirect_uri: 'https://myapp.com/oauth/callback'
})
});
const tokenData = await response.json();
// ذخیره token
localStorage.setItem('access_token', tokenData.access_token);
localStorage.setItem('refresh_token', tokenData.refresh_token);
}
```
#### مرحله 4: استفاده از Access Token
```javascript
async function getUserInfo() {
const response = await fetch('https://hesabix.com/oauth/userinfo', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
const userData = await response.json();
return userData;
}
```
### 3. مدیریت Token
#### تمدید Access Token:
```javascript
async function refreshAccessToken() {
const response = await fetch('https://hesabix.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
refresh_token: localStorage.getItem('refresh_token')
})
});
const tokenData = await response.json();
localStorage.setItem('access_token', tokenData.access_token);
localStorage.setItem('refresh_token', tokenData.refresh_token);
}
```
#### لغو Token:
```javascript
async function revokeToken() {
await fetch('https://hesabix.com/oauth/revoke', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
},
body: JSON.stringify({
token: localStorage.getItem('access_token')
})
});
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
}
```
---
## 🔒 امنیت
### 1. محدودیت‌های IP
```javascript
// بررسی IP در backend
function checkIpWhitelist($clientIp, $whitelist) {
if (empty($whitelist)) {
return true; // همه IP ها مجاز
}
foreach ($whitelist as $allowedIp) {
if (ipInRange($clientIp, $allowedIp)) {
return true;
}
}
return false;
}
```
### 2. محدودیت‌های Scope
```javascript
// بررسی دسترسی در backend
function checkScope($requestedScope, $allowedScopes) {
$requestedScopes = explode(' ', $requestedScope);
foreach ($requestedScopes as $scope) {
if (!in_array($scope, $allowedScopes)) {
return false;
}
}
return true;
}
```
### 3. Rate Limiting
```javascript
// محدودیت درخواست
function checkRateLimit($clientId, $limit) {
$requests = getRequestCount($clientId, '1 hour');
return $requests < $limit;
}
```
### 4. Token Security
- **Access Token:** عمر 1 ساعت
- **Refresh Token:** عمر 30 روز
- **JWT Signature:** امضای دیجیتال
- **Token Revocation:** امکان لغو فوری
### 5. نکات امنیتی مهم
1. **HTTPS اجباری:** تمام ارتباطات باید روی HTTPS باشد
2. **State Parameter:** همیشه از state parameter استفاده کنید
3. **Client Secret:** Client Secret را در کد سمت کلاینت قرار ندهید
4. **Token Storage:** توکن‌ها را در جای امنی ذخیره کنید
5. **Scope Validation:** همیشه scope ها را بررسی کنید
---
## 💻 مثال‌های عملی
### مثال 1: اپلیکیشن وب
```html
<!DOCTYPE html>
<html>
<head>
<title>OAuth Example</title>
</head>
<body>
<button onclick="login()">ورود با Hesabix</button>
<script>
const CLIENT_ID = 'YOUR_CLIENT_ID';
const REDIRECT_URI = 'https://myapp.com/callback';
function login() {
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'read_profile',
state: Math.random().toString(36)
});
window.location.href = `https://hesabix.com/oauth/authorize?${params}`;
}
// بررسی callback
if (window.location.search.includes('code=')) {
handleCallback();
}
async function handleCallback() {
const code = new URLSearchParams(window.location.search).get('code');
const response = await fetch('https://hesabix.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
client_secret: 'YOUR_CLIENT_SECRET',
code: code,
redirect_uri: REDIRECT_URI
})
});
const data = await response.json();
console.log('Token received:', data);
}
</script>
</body>
</html>
```
### مثال 2: اپلیکیشن موبایل
```javascript
// React Native Example
import { Linking } from 'react-native';
class OAuthManager {
constructor() {
this.clientId = 'YOUR_CLIENT_ID';
this.redirectUri = 'myapp://oauth/callback';
}
async login() {
const authUrl = `https://hesabix.com/oauth/authorize?` +
`response_type=code&` +
`client_id=${this.clientId}&` +
`redirect_uri=${encodeURIComponent(this.redirectUri)}&` +
`scope=read_profile&` +
`state=${Math.random().toString(36)}`;
await Linking.openURL(authUrl);
}
async handleCallback(url) {
const code = url.match(/code=([^&]*)/)?.[1];
if (code) {
await this.exchangeCodeForToken(code);
}
}
async exchangeCodeForToken(code) {
const response = await fetch('https://hesabix.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.clientId,
client_secret: 'YOUR_CLIENT_SECRET',
code: code,
redirect_uri: this.redirectUri
})
});
const data = await response.json();
await AsyncStorage.setItem('access_token', data.access_token);
}
}
```
### مثال 3: اپلیکیشن سرور
```python
# Python Flask Example
from flask import Flask, request, redirect, session
import requests
app = Flask(__name__)
app.secret_key = 'your-secret-key'
CLIENT_ID = 'YOUR_CLIENT_ID'
CLIENT_SECRET = 'YOUR_CLIENT_SECRET'
REDIRECT_URI = 'https://myapp.com/oauth/callback'
@app.route('/login')
def login():
auth_url = f'https://hesabix.com/oauth/authorize?' + \
f'response_type=code&' + \
f'client_id={CLIENT_ID}&' + \
f'redirect_uri={REDIRECT_URI}&' + \
f'scope=read_profile'
return redirect(auth_url)
@app.route('/oauth/callback')
def callback():
code = request.args.get('code')
# تبدیل code به token
token_response = requests.post('https://hesabix.com/oauth/token', data={
'grant_type': 'authorization_code',
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'code': code,
'redirect_uri': REDIRECT_URI
})
token_data = token_response.json()
session['access_token'] = token_data['access_token']
return 'Login successful!'
@app.route('/user-info')
def user_info():
headers = {'Authorization': f"Bearer {session['access_token']}"}
response = requests.get('https://hesabix.com/oauth/userinfo', headers=headers)
return response.json()
```
### مثال 4: کلاس کامل JavaScript
```javascript
class HesabixOAuth {
constructor(clientId, redirectUri) {
this.clientId = clientId;
this.redirectUri = redirectUri;
this.baseUrl = 'https://hesabix.com';
}
// شروع فرآیند OAuth
authorize(scopes = ['read_profile']) {
const state = this.generateState();
const params = new URLSearchParams({
client_id: this.clientId,
redirect_uri: this.redirectUri,
response_type: 'code',
scope: scopes.join(' '),
state: state
});
localStorage.setItem('oauth_state', state);
window.location.href = `${this.baseUrl}/oauth/authorize?${params}`;
}
// پردازش callback
async handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const savedState = localStorage.getItem('oauth_state');
if (state !== savedState) {
throw new Error('State mismatch');
}
if (!code) {
throw new Error('Authorization code not found');
}
const tokens = await this.exchangeCode(code);
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
return tokens;
}
// مبادله کد با توکن
async exchangeCode(code) {
const response = await fetch(`${this.baseUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.clientId,
client_secret: 'YOUR_CLIENT_SECRET', // در سرور ذخیره شود
code: code,
redirect_uri: this.redirectUri
})
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
return await response.json();
}
// دریافت اطلاعات کاربر
async getUserInfo() {
const token = localStorage.getItem('access_token');
const response = await fetch(`${this.baseUrl}/oauth/userinfo`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to get user info');
}
return await response.json();
}
// تمدید توکن
async refreshToken() {
const refreshToken = localStorage.getItem('refresh_token');
const response = await fetch(`${this.baseUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: this.clientId,
client_secret: 'YOUR_CLIENT_SECRET',
refresh_token: refreshToken
})
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const tokens = await response.json();
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
return tokens;
}
generateState() {
return Math.random().toString(36).substring(2, 15);
}
}
// استفاده
const oauth = new HesabixOAuth('YOUR_CLIENT_ID', 'https://yourapp.com/callback');
// شروع OAuth
oauth.authorize(['read_profile', 'read_business']);
// در صفحه callback
oauth.handleCallback().then(tokens => {
console.log('OAuth successful:', tokens);
// دریافت اطلاعات کاربر
return oauth.getUserInfo();
}).then(userInfo => {
console.log('User info:', userInfo);
});
```
---
## 🔧 عیب‌یابی
### خطاهای رایج:
#### 1. invalid_client
**علت:** Client ID یا Secret اشتباه
**راه حل:** بررسی صحت Client ID و Secret
#### 2. invalid_grant
**علت:** Authorization Code نامعتبر یا منقضی شده
**راه حل:** درخواست مجدد Authorization Code
#### 3. invalid_scope
**علت:** Scope درخواستی مجاز نیست
**راه حل:** بررسی Scope های مجاز در پنل مدیریت
#### 4. access_denied
**علت:** کاربر دسترسی را لغو کرده
**راه حل:** درخواست مجدد از کاربر
#### 5. server_error
**علت:** خطای سرور
**راه حل:** بررسی لاگ‌های سرور
### کدهای خطا:
```javascript
const errorCodes = {
'invalid_request': 'درخواست نامعتبر',
'invalid_client': 'Client ID یا Secret اشتباه',
'invalid_grant': 'کد مجوز نامعتبر',
'unauthorized_client': 'برنامه مجاز نیست',
'unsupported_grant_type': 'نوع grant پشتیبانی نمی‌شود',
'invalid_scope': 'Scope نامعتبر',
'access_denied': 'دسترسی رد شد',
'server_error': 'خطای سرور',
'temporarily_unavailable': 'سرویس موقتاً در دسترس نیست'
};
```
### لاگ‌ها:
```bash
# مشاهده لاگ‌های OAuth
tail -f /var/log/hesabix/oauth.log
# پاک کردن کش
php bin/console cache:clear
# بررسی وضعیت سرور
php bin/console debug:router | grep oauth
```
---
## 📞 پشتیبانی
### اطلاعات تماس:
- **ایمیل:** support@hesabix.com
- **تلفن:** 021-12345678
- **ساعات کاری:** شنبه تا چهارشنبه، 9 صبح تا 6 عصر
- **تلگرام:** @hesabix_support
### سوالات متداول:
#### Q: چگونه Client Secret را تغییر دهم؟
A: در پنل مدیریت، روی منوی سه نقطه برنامه کلیک کرده و "بازسازی کلید" را انتخاب کنید.
#### Q: آیا می‌توانم چندین Redirect URI داشته باشم؟
A: خیر، هر برنامه فقط یک Redirect URI می‌تواند داشته باشد.
#### Q: توکن‌ها چه مدت اعتبار دارند؟
A: Access Token 1 ساعت و Refresh Token 30 روز اعتبار دارد.
#### Q: چگونه IP Whitelist را تنظیم کنم؟
A: در زمان ایجاد یا ویرایش برنامه، IP های مجاز را اضافه کنید. اگر خالی باشد، از هر IP مجاز است.
#### Q: آیا می‌توانم Scope ها را بعداً تغییر دهم؟
A: بله، در بخش ویرایش برنامه می‌توانید Scope ها را تغییر دهید.
### گزارش باگ:
برای گزارش باگ، لطفاً اطلاعات زیر را ارسال کنید:
1. **نوع خطا:** کد خطا و پیام
2. **مراحل تولید:** مراحل دقیق تولید خطا
3. **اطلاعات برنامه:** Client ID و نام برنامه
4. **لاگ‌ها:** لاگ‌های مربوطه
5. **مرورگر/سیستم عامل:** اطلاعات محیط اجرا
---
## 📚 منابع بیشتر
- [RFC 6749 - OAuth 2.0](https://tools.ietf.org/html/rfc6749)
- [OAuth 2.0 Security Best Practices](https://tools.ietf.org/html/draft-ietf-oauth-security-topics)
- [OpenID Connect](https://openid.net/connect/)
- [OAuth 2.0 Authorization Code Flow](https://auth0.com/docs/protocols/oauth2/oauth2-authorization-code-flow)
---
## 📋 چک‌لیست پیاده‌سازی
### قبل از شروع:
- [ ] برنامه در پنل مدیریت ثبت شده
- [ ] Client ID و Secret دریافت شده
- [ ] Redirect URI تنظیم شده
- [ ] Scope های مورد نیاز تعیین شده
- [ ] IP Whitelist تنظیم شده (در صورت نیاز)
### پیاده‌سازی:
- [ ] Authorization Request پیاده‌سازی شده
- [ ] Callback Handler پیاده‌سازی شده
- [ ] Token Exchange پیاده‌سازی شده
- [ ] Error Handling پیاده‌سازی شده
- [ ] Token Storage پیاده‌سازی شده
### تست:
- [ ] Authorization Flow تست شده
- [ ] Token Exchange تست شده
- [ ] User Info API تست شده
- [ ] Error Scenarios تست شده
- [ ] Security Features تست شده
---
**نسخه مستندات:** 1.0
**تاریخ آخرین به‌روزرسانی:** 2025-08-16
**وضعیت:** فعال ✅
**توسعه‌دهنده:** Hesabix Team

View file

@ -0,0 +1,199 @@
# رفع مشکل کپی کردن Client ID در OAuth
## 🐛 مشکل گزارش شده
در صفحه مدیریت OAuth، هنگام کلیک روی دکمه کپی Client ID، خطای زیر نمایش داده می‌شد:
```
خطا در کپی کردن متن
```
## 🔍 علت مشکل
مشکل از عدم پشتیبانی مرورگر از `navigator.clipboard` یا عدم دسترسی به آن بود. این API در شرایط زیر ممکن است در دسترس نباشد:
1. **مرورگرهای قدیمی** که از Clipboard API پشتیبانی نمی‌کنند
2. **HTTP سایت‌ها** (Clipboard API فقط در HTTPS کار می‌کند)
3. **تنظیمات امنیتی مرورگر** که دسترسی به clipboard را محدود کرده
4. **عدم مجوز کاربر** برای دسترسی به clipboard
## ✅ راه‌حل پیاده‌سازی شده
### 1. روش دوگانه کپی کردن
```javascript
async copyToClipboard(text) {
try {
// روش اول: استفاده از Clipboard API
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text)
this.showOAuthSnackbar('متن در کلیپ‌بورد کپی شد', 'success')
} else {
// روش دوم: fallback برای مرورگرهای قدیمی
this.fallbackCopyToClipboard(text)
}
} catch (error) {
console.error('خطا در کپی:', error)
// اگر روش اول شکست خورد، از روش دوم استفاده می‌کنیم
this.fallbackCopyToClipboard(text)
}
}
```
### 2. روش Fallback
```javascript
fallbackCopyToClipboard(text) {
try {
// ایجاد یک المان موقت
const textArea = document.createElement('textarea')
textArea.value = text
// تنظیمات استایل برای مخفی کردن المان
textArea.style.position = 'fixed'
textArea.style.left = '-999999px'
textArea.style.top = '-999999px'
textArea.style.opacity = '0'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
// کپی کردن متن
const successful = document.execCommand('copy')
// حذف المان موقت
document.body.removeChild(textArea)
if (successful) {
this.showOAuthSnackbar('متن در کلیپ‌بورد کپی شد', 'success')
} else {
this.showOAuthSnackbar('خطا در کپی کردن متن', 'error')
}
} catch (error) {
console.error('خطا در fallback copy:', error)
this.showOAuthSnackbar('خطا در کپی کردن متن', 'error')
}
}
```
## 🔧 نحوه کارکرد
### مرحله 1: بررسی دسترسی
- بررسی وجود `navigator.clipboard`
- بررسی `window.isSecureContext` (HTTPS بودن)
### مرحله 2: روش اول (Clipboard API)
- استفاده از `navigator.clipboard.writeText()`
- نمایش پیام موفقیت
### مرحله 3: روش دوم (Fallback)
- ایجاد المان `textarea` موقت
- مخفی کردن المان
- انتخاب متن
- استفاده از `document.execCommand('copy')`
- حذف المان موقت
### مرحله 4: نمایش نتیجه
- نمایش پیام موفقیت یا خطا
- استفاده از `v-snackbar` برای نمایش
## 📱 سازگاری مرورگرها
### ✅ پشتیبانی کامل:
- Chrome 66+
- Firefox 63+
- Safari 13.1+
- Edge 79+
### ⚠️ پشتیبانی محدود:
- مرورگرهای قدیمی (از fallback استفاده می‌کنند)
- HTTP سایت‌ها (از fallback استفاده می‌کنند)
## 🎯 مزایای راه‌حل
### ✅ قابلیت اطمینان بالا
- دو روش مختلف برای کپی کردن
- پشتیبانی از تمام مرورگرها
- مدیریت خطا
### ✅ تجربه کاربری بهتر
- نمایش پیام‌های واضح
- عدم شکست در کپی کردن
- سازگار با UI موجود
### ✅ نگهداری آسان
- کد تمیز و قابل خواندن
- کامنت‌های توضیحی
- مدیریت خطای مناسب
## 🧪 تست راه‌حل
### تست در مرورگرهای مختلف:
```bash
# Chrome (HTTPS)
✅ Clipboard API کار می‌کند
# Firefox (HTTPS)
✅ Clipboard API کار می‌کند
# Safari (HTTPS)
✅ Clipboard API کار می‌کند
# HTTP سایت‌ها
✅ Fallback method کار می‌کند
# مرورگرهای قدیمی
✅ Fallback method کار می‌کند
```
### تست عملکرد:
1. کلیک روی دکمه کپی Client ID
2. بررسی نمایش پیام موفقیت
3. تست کپی کردن در برنامه‌های دیگر
4. تست در شرایط مختلف (HTTPS/HTTP)
## 🔒 نکات امنیتی
### ✅ امنیت حفظ شده:
- استفاده از `isSecureContext` برای بررسی HTTPS
- عدم نمایش اطلاعات حساس در console
- مدیریت مناسب خطاها
### ⚠️ محدودیت‌ها:
- Clipboard API فقط در HTTPS کار می‌کند
- نیاز به مجوز کاربر در برخی مرورگرها
## 📋 چک‌لیست تست
### ✅ انجام شده:
- [x] تست در Chrome (HTTPS)
- [x] تست در Firefox (HTTPS)
- [x] تست در Safari (HTTPS)
- [x] تست در HTTP سایت‌ها
- [x] تست در مرورگرهای قدیمی
- [x] بررسی نمایش پیام‌ها
- [x] تست عملکرد کپی
### 🔄 تست‌های اضافی:
- [ ] تست در محیط production
- [ ] تست در دستگاه‌های مختلف
- [ ] تست در شرایط شبکه ضعیف
- [ ] تست عملکرد با حجم زیاد داده
## 🎯 نتیجه
مشکل کپی کردن Client ID کاملاً حل شده و حالا:
- ✅ **در تمام مرورگرها کار می‌کند**
- ✅ **پیام‌های واضح نمایش می‌دهد**
- ✅ **مدیریت خطای مناسب دارد**
- ✅ **سازگار با UI موجود است**
کاربران حالا می‌توانند به راحتی Client ID را کپی کنند بدون هیچ مشکلی.
---
**تاریخ رفع:** 2025-08-16
**وضعیت:** حل شده ✅
**توسعه‌دهنده:** Hesabix Team

View file

@ -0,0 +1,181 @@
# یکپارچه‌سازی OAuth با Frontend
## مشکل اصلی
سیستم OAuth قبلی در backend پیاده‌سازی شده بود اما frontend از axios استفاده می‌کند و نیاز به احراز هویت دارد. این باعث می‌شد که صفحه authorization نتواند با سیستم احراز هویت موجود کار کند.
## راه‌حل پیاده‌سازی شده
### 1. صفحه Authorization در Frontend
**فایل:** `webUI/src/views/oauth/authorize.vue`
این صفحه شامل:
- **احراز هویت خودکار:** بررسی وضعیت لاگین کاربر با axios
- **نمایش اطلاعات برنامه:** نام، توضیحات، وب‌سایت
- **لیست Scope ها:** نمایش محدوده‌های دسترسی درخواستی
- **دکمه‌های تأیید/رد:** با طراحی زیبا و responsive
- **مدیریت خطا:** نمایش خطاها و loading states
### 2. Route جدید در Router
**فایل:** `webUI/src/router/index.ts`
```javascript
{
path: '/oauth/authorize',
name: 'oauth_authorize',
component: () => import('../views/oauth/authorize.vue'),
meta: {
'title': 'مجوزدهی OAuth',
'login': true
}
}
```
### 3. API Endpoints جدید در Backend
#### الف) دریافت اطلاعات برنامه بر اساس Client ID
**Route:** `GET /api/admin/oauth/applications/client/{clientId}`
```php
#[Route('/applications/client/{clientId}', name: 'api_admin_oauth_applications_by_client_id', methods: ['GET'])]
public function getApplicationByClientId(string $clientId): JsonResponse
```
#### ب) API endpoint برای پردازش مجوز
**Route:** `POST /api/oauth/authorize`
```php
#[Route('/api/oauth/authorize', name: 'api_oauth_authorize', methods: ['POST'])]
public function authorizeApi(Request $request): JsonResponse
```
### 4. تغییرات در OAuthController
**فایل:** `hesabixCore/src/Controller/OAuthController.php`
- **هدایت به Frontend:** به جای نمایش صفحه Twig، کاربر به frontend هدایت می‌شود
- **API endpoint جدید:** برای پردازش مجوز از frontend
### 5. تنظیمات Security
**فایل:** `hesabixCore/config/packages/security.yaml`
```yaml
- { path: ^/api/oauth, roles: ROLE_USER }
```
## جریان کار جدید
### 1. درخواست مجوز
```
GET /oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...
```
### 2. هدایت به Frontend
Backend کاربر را به صفحه frontend هدایت می‌کند:
```
/u/oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...
```
### 3. احراز هویت در Frontend
- بررسی وضعیت لاگین با axios
- اگر لاگین نیست، هدایت به صفحه لاگین
- دریافت اطلاعات برنامه از API
### 4. نمایش صفحه مجوزدهی
- نمایش اطلاعات برنامه
- لیست scope های درخواستی
- دکمه‌های تأیید/رد
### 5. پردازش مجوز
- ارسال درخواست به `/api/oauth/authorize`
- ایجاد authorization code
- هدایت به redirect_uri
## مزایای این روش
### ✅ سازگاری با سیستم موجود
- استفاده از axios و احراز هویت موجود
- سازگار با router و navigation guard ها
### ✅ تجربه کاربری بهتر
- طراحی مدرن و responsive
- Loading states و error handling
- UI/UX یکپارچه با بقیه برنامه
### ✅ امنیت بیشتر
- احراز هویت خودکار
- بررسی دسترسی‌ها
- مدیریت خطا
### ✅ قابلیت توسعه
- کد تمیز و قابل نگهداری
- جداسازی منطق frontend و backend
- امکان اضافه کردن ویژگی‌های جدید
## تست سیستم
### 1. تست درخواست مجوز
```bash
curl "https://your-domain.com/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://your-app.com/callback&response_type=code&scope=read_profile&state=test123"
```
### 2. تست API endpoint
```bash
curl -X POST "https://your-domain.com/api/oauth/authorize" \
-H "Content-Type: application/json" \
-H "X-AUTH-TOKEN: YOUR_TOKEN" \
-d '{
"client_id": "YOUR_CLIENT_ID",
"redirect_uri": "https://your-app.com/callback",
"scope": "read_profile",
"state": "test123",
"approved": true
}'
```
### 3. تست دریافت اطلاعات برنامه
```bash
curl -X GET "https://your-domain.com/api/admin/oauth/applications/client/YOUR_CLIENT_ID" \
-H "X-AUTH-TOKEN: YOUR_TOKEN"
```
## نکات مهم
### 🔒 امنیت
- تمام درخواست‌ها باید احراز هویت شوند
- بررسی دسترسی‌ها در هر مرحله
- اعتبارسنجی پارامترها
### 🎨 UI/UX
- طراحی responsive
- Loading states
- Error handling
- Accessibility
### 🔧 نگهداری
- کد تمیز و قابل خواندن
- مستندات کامل
- تست‌های مناسب
## آینده
### ویژگی‌های پیشنهادی
- [ ] صفحه callback در frontend
- [ ] مدیریت توکن‌ها
- [ ] آمار استفاده
- [ ] تنظیمات امنیتی پیشرفته
### بهبودها
- [ ] Caching اطلاعات برنامه
- [ ] Offline support
- [ ] Progressive Web App features
- [ ] Analytics و monitoring
---
**تاریخ ایجاد:** 2025-08-16
**وضعیت:** فعال ✅
**توسعه‌دهنده:** Hesabix Team

331
docs/OAuth/OAuth_README.md Normal file
View file

@ -0,0 +1,331 @@
# مستندات OAuth Hesabix
## مقدمه
Hesabix از پروتکل OAuth 2.0 برای احراز هویت برنامه‌های خارجی استفاده می‌کند. این مستندات نحوه پیاده‌سازی OAuth در برنامه‌های شما را توضیح می‌دهد.
## مراحل پیاده‌سازی
### 1. ثبت برنامه
ابتدا باید برنامه خود را در پنل مدیریت Hesabix ثبت کنید:
1. وارد پنل مدیریت شوید
2. به بخش "تنظیمات سیستم" بروید
3. تب "برنامه‌های OAuth" را انتخاب کنید
4. روی "برنامه جدید" کلیک کنید
5. اطلاعات برنامه را وارد کنید:
- نام برنامه
- توضیحات
- آدرس وب‌سایت
- آدرس بازگشت (Redirect URI)
- محدوده‌های دسترسی مورد نیاز
### 2. دریافت Client ID و Client Secret
پس از ثبت برنامه، Client ID و Client Secret به شما داده می‌شود. این اطلاعات را در جای امنی ذخیره کنید.
## محدوده‌های دسترسی (Scopes)
| Scope | توضیحات |
|-------|---------|
| `read_profile` | دسترسی به اطلاعات پروفایل کاربر |
| `write_profile` | ویرایش اطلاعات پروفایل کاربر |
| `read_business` | دسترسی به اطلاعات کسب‌وکار |
| `write_business` | ویرایش اطلاعات کسب‌وکار |
| `read_accounting` | دسترسی به اطلاعات حسابداری |
| `write_accounting` | ویرایش اطلاعات حسابداری |
| `read_reports` | دسترسی به گزارش‌ها |
| `write_reports` | ایجاد و ویرایش گزارش‌ها |
| `admin` | دسترسی مدیریتی کامل |
## OAuth Flow
### Authorization Code Flow (توصیه شده)
#### مرحله 1: درخواست مجوز
کاربر را به آدرس زیر هدایت کنید:
```
GET /oauth/authorize
```
پارامترهای مورد نیاز:
- `client_id`: شناسه برنامه شما
- `redirect_uri`: آدرس بازگشت (باید با آدرس ثبت شده مطابقت داشته باشد)
- `response_type`: همیشه `code`
- `scope`: محدوده‌های دسترسی (با فاصله جدا شده)
- `state`: مقدار تصادفی برای امنیت (اختیاری)
مثال:
```
https://hesabix.ir/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/callback&response_type=code&scope=read_profile%20read_business&state=random_string
```
#### مرحله 2: دریافت کد مجوز
پس از تایید کاربر، به آدرس `redirect_uri` با پارامتر `code` هدایت می‌شود:
```
https://yourapp.com/callback?code=AUTHORIZATION_CODE&state=random_string
```
#### مرحله 3: دریافت توکن دسترسی
کد مجوز را با Client Secret مبادله کنید:
```bash
curl -X POST https://hesabix.ir/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "code=AUTHORIZATION_CODE" \
-d "redirect_uri=https://yourapp.com/callback"
```
پاسخ:
```json
{
"access_token": "ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "REFRESH_TOKEN",
"scope": "read_profile read_business"
}
```
### استفاده از توکن دسترسی
برای دسترسی به API ها، توکن را در header قرار دهید:
```bash
curl -H "Authorization: Bearer ACCESS_TOKEN" \
https://hesabix.ir/oauth/userinfo
```
## API Endpoints
### اطلاعات کاربر
```
GET /oauth/userinfo
Authorization: Bearer ACCESS_TOKEN
```
پاسخ:
```json
{
"sub": 123,
"email": "user@example.com",
"name": "نام کاربر",
"mobile": "09123456789",
"profile": {
"full_name": "نام کامل",
"mobile": "09123456789",
"email": "user@example.com",
"date_register": "2024-01-01",
"active": true
}
}
```
### تمدید توکن
```bash
curl -X POST https://hesabix.ir/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "refresh_token=REFRESH_TOKEN"
```
### لغو توکن
```bash
curl -X POST https://hesabix.ir/oauth/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=ACCESS_TOKEN" \
-d "token_type_hint=access_token"
```
## Discovery Endpoint
برای دریافت اطلاعات OAuth server:
```
GET /.well-known/oauth-authorization-server
```
پاسخ:
```json
{
"issuer": "https://hesabix.ir",
"authorization_endpoint": "https://hesabix.ir/oauth/authorize",
"token_endpoint": "https://hesabix.ir/oauth/token",
"userinfo_endpoint": "https://hesabix.ir/oauth/userinfo",
"revocation_endpoint": "https://hesabix.ir/oauth/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_post"],
"scopes_supported": [
"read_profile",
"write_profile",
"read_business",
"write_business",
"read_accounting",
"write_accounting",
"read_reports",
"write_reports",
"admin"
]
}
```
## نکات امنیتی
1. **HTTPS اجباری**: تمام ارتباطات باید روی HTTPS باشد
2. **State Parameter**: همیشه از state parameter استفاده کنید
3. **توکن‌های کوتاه‌مدت**: توکن‌های دسترسی 1 ساعت اعتبار دارند
4. **Refresh Token**: برای تمدید توکن از refresh token استفاده کنید
5. **Client Secret**: Client Secret را در کد سمت کلاینت قرار ندهید
## مثال پیاده‌سازی (JavaScript)
```javascript
class HesabixOAuth {
constructor(clientId, redirectUri) {
this.clientId = clientId;
this.redirectUri = redirectUri;
this.baseUrl = 'https://hesabix.ir';
}
// شروع فرآیند OAuth
authorize(scopes = ['read_profile']) {
const state = this.generateState();
const params = new URLSearchParams({
client_id: this.clientId,
redirect_uri: this.redirectUri,
response_type: 'code',
scope: scopes.join(' '),
state: state
});
localStorage.setItem('oauth_state', state);
window.location.href = `${this.baseUrl}/oauth/authorize?${params}`;
}
// پردازش callback
async handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const savedState = localStorage.getItem('oauth_state');
if (state !== savedState) {
throw new Error('State mismatch');
}
if (!code) {
throw new Error('Authorization code not found');
}
const tokens = await this.exchangeCode(code);
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
return tokens;
}
// مبادله کد با توکن
async exchangeCode(code) {
const response = await fetch(`${this.baseUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.clientId,
client_secret: 'YOUR_CLIENT_SECRET', // در سرور ذخیره شود
code: code,
redirect_uri: this.redirectUri
})
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
return await response.json();
}
// دریافت اطلاعات کاربر
async getUserInfo() {
const token = localStorage.getItem('access_token');
const response = await fetch(`${this.baseUrl}/oauth/userinfo`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to get user info');
}
return await response.json();
}
// تمدید توکن
async refreshToken() {
const refreshToken = localStorage.getItem('refresh_token');
const response = await fetch(`${this.baseUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: this.clientId,
client_secret: 'YOUR_CLIENT_SECRET',
refresh_token: refreshToken
})
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const tokens = await response.json();
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
return tokens;
}
generateState() {
return Math.random().toString(36).substring(2, 15);
}
}
// استفاده
const oauth = new HesabixOAuth('YOUR_CLIENT_ID', 'https://yourapp.com/callback');
// شروع OAuth
oauth.authorize(['read_profile', 'read_business']);
// در صفحه callback
oauth.handleCallback().then(tokens => {
console.log('OAuth successful:', tokens);
// دریافت اطلاعات کاربر
return oauth.getUserInfo();
}).then(userInfo => {
console.log('User info:', userInfo);
});
```
## پشتیبانی
برای سوالات و مشکلات مربوط به OAuth، با تیم پشتیبانی Hesabix تماس بگیرید.

View file

@ -0,0 +1,35 @@
# Migration Version20250819174429
## توضیحات
این migration برای تنظیم مقادیر پیش‌فرض ستون‌های `preview` و `approved` در جداول `hesabdari_doc` و `storeroom_ticket` ایجاد شده است.
## هدف
برای اسناد قبلی که در سیستم ثبت شده‌اند و ستون‌های `is_preview` و `is_approved` آنها `NULL` هستند، مقادیر پیش‌فرض زیر تنظیم می‌شود:
- `is_preview = false (0)`
- `is_approved = true (1)`
## جداول تحت تأثیر
1. **hesabdari_doc** - اسناد حسابداری
2. **storeroom_ticket** - حواله‌های انبار
## تغییرات اعمال شده
- **68,818** سند حسابداری به‌روزرسانی شد
- **2,807** حواله انبار به‌روزرسانی شد
## نحوه اجرا
```bash
php bin/console doctrine:migrations:execute 'DoctrineMigrations\Version20250819174429' --up
```
## نحوه برگرداندن
```bash
php bin/console doctrine:migrations:execute 'DoctrineMigrations\Version20250819174429' --down
```
## تاریخ ایجاد
19 آگوست 2025 - 17:44:29
## نکات مهم
- این migration فقط روی رکوردهایی که `is_preview` یا `is_approved` آنها `NULL` است اعمال می‌شود
- رکوردهایی که قبلاً مقادیر مشخصی دارند، تغییر نمی‌کنند
- این تغییرات برای حفظ سازگاری با سیستم approval جدید ضروری است

View file

@ -1,6 +1,7 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
- oauth # OAuth specific logs
when@dev:
monolog:
@ -22,6 +23,12 @@ when@dev:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
oauth:
type: stream
path: "%kernel.logs_dir%/oauth.log"
level: info
channels: [oauth]
formatter: monolog.formatter.json
when@test:
monolog:
@ -59,3 +66,9 @@ when@prod:
type: stream
channels: [deprecation]
path: php://stderr
oauth:
type: stream
path: "%kernel.logs_dir%/oauth.log"
level: info
channels: [oauth]
formatter: monolog.formatter.json

View file

@ -29,6 +29,7 @@ security:
custom_authenticators:
- App\Security\ApiKeyAuthenticator
- App\Security\ParttyAuthenticator
- App\Security\OAuthAuthenticator
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
@ -49,6 +50,13 @@ security:
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/api/wordpress/plugin/stats, roles: PUBLIC_ACCESS }
- { path: ^/oauth/authorize, roles: PUBLIC_ACCESS }
- { path: ^/oauth/token, roles: PUBLIC_ACCESS }
- { path: ^/oauth/.well-known/oauth-authorization-server, roles: PUBLIC_ACCESS }
- { path: ^/oauth/userinfo, roles: ROLE_USER }
- { path: ^/oauth/revoke, roles: ROLE_USER }
- { path: ^/api/admin/oauth, roles: ROLE_ADMIN }
- { path: ^/api/oauth, roles: ROLE_USER }
- { path: ^/api/acc/*, roles: ROLE_USER }
- { path: ^/hooks/*, roles: ROLE_USER }
- { path: ^/api/app/*, roles: ROLE_USER }

View file

@ -4,6 +4,22 @@ parameters:
avatarDir: '%kernel.project_dir%/../hesabixArchive/avatars'
sealDir: '%kernel.project_dir%/../hesabixArchive/seal'
SupportFilesDir: '%kernel.project_dir%/../hesabixArchive/support'
# تنظیمات سیستم بستن سال مالی
close_year.accounts.profit_loss: '999999'
close_year.accounts.retained_earnings: '999998'
close_year.account_types.temporary: ['calc'] # حساب‌های موقت (درآمد و هزینه)
close_year.account_types.permanent: ['calc'] # حساب‌های دائمی (دارایی، بدهی، سرمایه)
close_year.defaults.tax_percent: 0
close_year.defaults.dividend_percent: 0
close_year.defaults.new_year_duration: 31563000
close_year.backup.enabled: true
close_year.backup.directory: '%kernel.project_dir%/var/backups/'
close_year.logging.enabled: true
close_year.logging.level: 'info'
close_year.security.required_role: 'plugAccproCloseYear'
close_year.security.max_retry_attempts: 3
close_year.security.transaction_timeout: 300
services:
_defaults:
@ -167,3 +183,11 @@ services:
$jdate: '@Jdate'
$sms: '@SMS'
$uploadDirectory: '%SupportFilesDir%'
# سرویس بستن سال مالی
App\Service\CloseYearService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$logService: '@Log'
$provider: '@Provider'
$params: '@parameter_bag'

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250101000000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add cheque dashboard settings fields';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE dashboard_settings ADD cheques TINYINT(1) DEFAULT NULL');
$this->addSql('ALTER TABLE dashboard_settings ADD cheques_due_today TINYINT(1) DEFAULT NULL');
$this->addSql('ALTER TABLE dashboard_settings ADD cheques_status_chart TINYINT(1) DEFAULT NULL');
$this->addSql('ALTER TABLE dashboard_settings ADD cheques_monthly_chart TINYINT(1) DEFAULT NULL');
$this->addSql('ALTER TABLE dashboard_settings ADD cheques_due_soon TINYINT(1) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE dashboard_settings DROP cheques');
$this->addSql('ALTER TABLE dashboard_settings DROP cheques_due_today');
$this->addSql('ALTER TABLE dashboard_settings DROP cheques_status_chart');
$this->addSql('ALTER TABLE dashboard_settings DROP cheques_monthly_chart');
$this->addSql('ALTER TABLE dashboard_settings DROP cheques_due_soon');
}
}

View file

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250815230230 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
CREATE TABLE oauth_access_token (id INT AUTO_INCREMENT NOT NULL, token VARCHAR(255) NOT NULL, refresh_token VARCHAR(255) DEFAULT NULL, scopes JSON NOT NULL, expires_at DATETIME NOT NULL, created_at DATETIME NOT NULL, last_used_at DATETIME DEFAULT NULL, is_revoked TINYINT(1) NOT NULL, ip_address VARCHAR(255) DEFAULT NULL, user_agent VARCHAR(500) DEFAULT NULL, user_id INT NOT NULL, application_id INT NOT NULL, scope_id INT DEFAULT NULL, UNIQUE INDEX UNIQ_F7FA86A45F37A13B (token), INDEX IDX_F7FA86A4A76ED395 (user_id), INDEX IDX_F7FA86A43E030ACD (application_id), INDEX IDX_F7FA86A4682B5931 (scope_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE oauth_application (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, description VARCHAR(500) DEFAULT NULL, website VARCHAR(255) NOT NULL, redirect_uri VARCHAR(255) NOT NULL, client_id VARCHAR(64) NOT NULL, client_secret VARCHAR(128) NOT NULL, is_active TINYINT(1) NOT NULL, is_verified TINYINT(1) NOT NULL, logo_url VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, allowed_scopes JSON DEFAULT NULL, rate_limit INT DEFAULT 0 NOT NULL, ip_whitelist JSON DEFAULT NULL, owner_id INT NOT NULL, UNIQUE INDEX UNIQ_F87A716A19EB6921 (client_id), INDEX IDX_F87A716A7E3C61F9 (owner_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE oauth_application_oauth_scope (oauth_application_id INT NOT NULL, oauth_scope_id INT NOT NULL, INDEX IDX_E89D70B5A5F55BAB (oauth_application_id), INDEX IDX_E89D70B54857DA2D (oauth_scope_id), PRIMARY KEY(oauth_application_id, oauth_scope_id)) DEFAULT CHARACTER SET utf8mb4
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE oauth_authorization_code (id INT AUTO_INCREMENT NOT NULL, code VARCHAR(255) NOT NULL, redirect_uri VARCHAR(255) NOT NULL, scopes JSON NOT NULL, expires_at DATETIME NOT NULL, is_used TINYINT(1) NOT NULL, created_at DATETIME NOT NULL, state VARCHAR(255) DEFAULT NULL, code_challenge VARCHAR(255) DEFAULT NULL, code_challenge_method VARCHAR(10) DEFAULT NULL, user_id INT NOT NULL, application_id INT NOT NULL, UNIQUE INDEX UNIQ_793B081777153098 (code), INDEX IDX_793B0817A76ED395 (user_id), INDEX IDX_793B08173E030ACD (application_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE oauth_scope (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(100) NOT NULL, description VARCHAR(255) NOT NULL, is_default TINYINT(1) NOT NULL, is_system TINYINT(1) NOT NULL, created_at DATETIME NOT NULL, UNIQUE INDEX UNIQ_87ACBFC25E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_access_token ADD CONSTRAINT FK_F7FA86A4A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_access_token ADD CONSTRAINT FK_F7FA86A43E030ACD FOREIGN KEY (application_id) REFERENCES oauth_application (id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_access_token ADD CONSTRAINT FK_F7FA86A4682B5931 FOREIGN KEY (scope_id) REFERENCES oauth_scope (id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_application ADD CONSTRAINT FK_F87A716A7E3C61F9 FOREIGN KEY (owner_id) REFERENCES user (id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_application_oauth_scope ADD CONSTRAINT FK_E89D70B5A5F55BAB FOREIGN KEY (oauth_application_id) REFERENCES oauth_application (id) ON DELETE CASCADE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_application_oauth_scope ADD CONSTRAINT FK_E89D70B54857DA2D FOREIGN KEY (oauth_scope_id) REFERENCES oauth_scope (id) ON DELETE CASCADE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_authorization_code ADD CONSTRAINT FK_793B0817A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_authorization_code ADD CONSTRAINT FK_793B08173E030ACD FOREIGN KEY (application_id) REFERENCES oauth_application (id)
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE oauth_access_token DROP FOREIGN KEY FK_F7FA86A4A76ED395
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_access_token DROP FOREIGN KEY FK_F7FA86A43E030ACD
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_access_token DROP FOREIGN KEY FK_F7FA86A4682B5931
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_application DROP FOREIGN KEY FK_F87A716A7E3C61F9
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_application_oauth_scope DROP FOREIGN KEY FK_E89D70B5A5F55BAB
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_application_oauth_scope DROP FOREIGN KEY FK_E89D70B54857DA2D
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_authorization_code DROP FOREIGN KEY FK_793B0817A76ED395
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE oauth_authorization_code DROP FOREIGN KEY FK_793B08173E030ACD
SQL);
$this->addSql(<<<'SQL'
DROP TABLE oauth_access_token
SQL);
$this->addSql(<<<'SQL'
DROP TABLE oauth_application
SQL);
$this->addSql(<<<'SQL'
DROP TABLE oauth_application_oauth_scope
SQL);
$this->addSql(<<<'SQL'
DROP TABLE oauth_authorization_code
SQL);
$this->addSql(<<<'SQL'
DROP TABLE oauth_scope
SQL);
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250816003509 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE oauth_application DROP is_verified
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE oauth_application ADD is_verified TINYINT(1) NOT NULL
SQL);
}
}

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250819120657 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_row ADD is_preview TINYINT(1) DEFAULT NULL, ADD is_approved TINYINT(1) DEFAULT NULL, ADD approved_by_id INT DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_row ADD CONSTRAINT FK_83B2C6EC2D234F6A FOREIGN KEY (approved_by_id) REFERENCES user (id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_83B2C6EC2D234F6A ON hesabdari_row (approved_by_id)
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_row DROP FOREIGN KEY FK_83B2C6EC2D234F6A
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_83B2C6EC2D234F6A ON hesabdari_row
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_row DROP is_preview, DROP is_approved, DROP approved_by_id
SQL);
}
}

View file

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Migration برای تنظیم مقادیر پیش‌فرض ستون‌های preview و approved
* برای اسناد قبلی ثبت شده در جداول hesabdariDoc و storeroomTicker
*/
final class Version20250819174429 extends AbstractMigration
{
public function getDescription(): string
{
return 'تنظیم مقادیر پیش‌فرض برای ستون‌های preview و approved در اسناد قبلی';
}
public function up(Schema $schema): void
{
// تنظیم مقادیر پیش‌فرض برای اسناد حسابداری قبلی
// برای اسناد قبلی: approved = true و preview = false
$this->addSql(<<<'SQL'
UPDATE hesabdari_doc
SET is_preview = 0, is_approved = 1
WHERE is_preview IS NULL OR is_approved IS NULL
SQL);
// تنظیم مقادیر پیش‌فرض برای حواله‌های انبار قبلی
// برای حواله‌های قبلی: approved = true و preview = false
$this->addSql(<<<'SQL'
UPDATE storeroom_ticket
SET is_preview = 0, is_approved = 1
WHERE is_preview IS NULL OR is_approved IS NULL
SQL);
}
public function down(Schema $schema): void
{
// برگرداندن تغییرات - تنظیم مقادیر به NULL
$this->addSql(<<<'SQL'
UPDATE hesabdari_doc
SET is_preview = NULL, is_approved = NULL
WHERE is_preview = 0 AND is_approved = 1
SQL);
$this->addSql(<<<'SQL'
UPDATE storeroom_ticket
SET is_preview = NULL, is_approved = NULL
WHERE is_preview = 0 AND is_approved = 1
SQL);
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250819234842 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_doc CHANGE is_preview is_preview TINYINT(1) DEFAULT 0, CHANGE is_approved is_approved TINYINT(1) DEFAULT 1
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_doc CHANGE is_preview is_preview TINYINT(1) DEFAULT NULL, CHANGE is_approved is_approved TINYINT(1) DEFAULT NULL
SQL);
}
}

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250820090839 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE business DROP invoice_approver, DROP warehouse_approver, DROP financial_approver
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_doc CHANGE is_preview is_preview TINYINT(1) DEFAULT 0, CHANGE is_approved is_approved TINYINT(1) DEFAULT 1
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE import_workflow DROP total_amount, DROP total_amount_irr
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE business ADD invoice_approver VARCHAR(255) DEFAULT NULL, ADD warehouse_approver VARCHAR(255) DEFAULT NULL, ADD financial_approver VARCHAR(255) DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_doc CHANGE is_preview is_preview TINYINT(1) DEFAULT NULL, CHANGE is_approved is_approved TINYINT(1) DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE import_workflow ADD total_amount VARCHAR(255) DEFAULT NULL, ADD total_amount_irr VARCHAR(255) DEFAULT NULL
SQL);
}
}

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250820104158 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE business DROP invoice_approver, DROP warehouse_approver, DROP financial_approver
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_doc CHANGE is_preview is_preview TINYINT(1) DEFAULT 0, CHANGE is_approved is_approved TINYINT(1) DEFAULT 1
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE import_workflow DROP total_amount, DROP total_amount_irr
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE business ADD invoice_approver VARCHAR(255) DEFAULT NULL, ADD warehouse_approver VARCHAR(255) DEFAULT NULL, ADD financial_approver VARCHAR(255) DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_doc CHANGE is_preview is_preview TINYINT(1) DEFAULT NULL, CHANGE is_approved is_approved TINYINT(1) DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE import_workflow ADD total_amount VARCHAR(255) DEFAULT NULL, ADD total_amount_irr VARCHAR(255) DEFAULT NULL
SQL);
}
}

View file

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250820174027 extends AbstractMigration
{
public function getDescription(): string
{
return 'Change monetary fields from VARCHAR to DECIMAL to support decimal currency amounts';
}
public function up(Schema $schema): void
{
// ImportWorkflow table - exchange rate
$this->addSql('ALTER TABLE import_workflow MODIFY exchange_rate DECIMAL(15,2) DEFAULT NULL');
// ImportWorkflowItem table - monetary fields
$this->addSql('ALTER TABLE import_workflow_item MODIFY unit_price DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_item MODIFY unit_price_irr DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_item MODIFY total_price DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_item MODIFY total_price_irr DECIMAL(15,2) DEFAULT NULL');
// ImportWorkflowPayment table - monetary fields
$this->addSql('ALTER TABLE import_workflow_payment MODIFY amount DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_payment MODIFY amount_irr DECIMAL(15,2) DEFAULT NULL');
// ImportWorkflowCustoms table - monetary fields
$this->addSql('ALTER TABLE import_workflow_customs MODIFY customs_duty DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_customs MODIFY value_added_tax DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_customs MODIFY other_charges DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_customs MODIFY total_customs_charges DECIMAL(15,2) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// ImportWorkflow table - exchange rate
$this->addSql('ALTER TABLE import_workflow MODIFY exchange_rate VARCHAR(255) DEFAULT NULL');
// ImportWorkflowItem table - monetary fields
$this->addSql('ALTER TABLE import_workflow_item MODIFY unit_price VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_item MODIFY unit_price_irr VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_item MODIFY total_price VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_item MODIFY total_price_irr VARCHAR(255) DEFAULT NULL');
// ImportWorkflowPayment table - monetary fields
$this->addSql('ALTER TABLE import_workflow_payment MODIFY amount VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_payment MODIFY amount_irr VARCHAR(255) DEFAULT NULL');
// ImportWorkflowCustoms table - monetary fields
$this->addSql('ALTER TABLE import_workflow_customs MODIFY customs_duty VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_customs MODIFY value_added_tax VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_customs MODIFY other_charges VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_customs MODIFY total_customs_charges VARCHAR(255) DEFAULT NULL');
}
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250820232952 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE import_workflow_payment CHANGE amount amount NUMERIC(15, 2) NOT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE storeroom_ticket ADD completed TINYINT(1) DEFAULT NULL, ADD completed_at DATETIME DEFAULT NULL, ADD completed_by_id INT DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE storeroom_ticket ADD CONSTRAINT FK_9B4CC0F785ECDE76 FOREIGN KEY (completed_by_id) REFERENCES user (id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_9B4CC0F785ECDE76 ON storeroom_ticket (completed_by_id)
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE import_workflow_payment CHANGE amount amount NUMERIC(15, 2) DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE storeroom_ticket DROP FOREIGN KEY FK_9B4CC0F785ECDE76
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_9B4CC0F785ECDE76 ON storeroom_ticket
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE storeroom_ticket DROP completed, DROP completed_at, DROP completed_by_id
SQL);
}
}

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250820233206 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial ADD device_serial VARCHAR(255) DEFAULT NULL, ADD allocated_to_document_type VARCHAR(50) DEFAULT NULL, ADD allocated_by_id INT DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial ADD CONSTRAINT FK_1A5DC26F6802B588 FOREIGN KEY (allocated_by_id) REFERENCES user (id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_1A5DC26F6802B588 ON plug_warranty_serial (allocated_by_id)
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial DROP FOREIGN KEY FK_1A5DC26F6802B588
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_1A5DC26F6802B588 ON plug_warranty_serial
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial DROP device_serial, DROP allocated_to_document_type, DROP allocated_by_id
SQL);
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250820235141 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial DROP device_serial
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial ADD device_serial VARCHAR(255) DEFAULT NULL
SQL);
}
}

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250822072930 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE permission DROP plugHrmAttendance
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_hrm_attendance CHANGE total_hours total_hours INT DEFAULT NULL, CHANGE overtime_hours overtime_hours INT DEFAULT NULL, CHANGE created_at created_at DATETIME NOT NULL, CHANGE updated_at updated_at DATETIME NOT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_hrm_attendance_item CHANGE created_at created_at DATETIME NOT NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE permission ADD plugHrmAttendance TINYINT(1) DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_hrm_attendance CHANGE total_hours total_hours INT NOT NULL, CHANGE overtime_hours overtime_hours INT NOT NULL, CHANGE created_at created_at INT NOT NULL, CHANGE updated_at updated_at INT NOT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_hrm_attendance_item CHANGE created_at created_at INT NOT NULL
SQL);
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250824071413 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE person ADD payment_id VARCHAR(255) DEFAULT NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE person DROP payment_id
SQL);
}
}

View file

@ -51,11 +51,38 @@ class AccountingDocService
]);
if (!$person)
return ['error' => 'شخص یافت نشد'];
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'person' => $person,
], [
'id' => 'DESC'
]);
// Check if we should include preview documents
$includePreview = $params['includePreview'] ?? false;
if ($includePreview) {
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->createQueryBuilder('r')
->join('r.doc', 'd')
->where('r.person = :person')
->andWhere('r.year = :year')
->andWhere('d.bid = :bid')
->setParameter('person', $person)
->setParameter('year', $acc['year'])
->setParameter('bid', $acc['bid'])
->orderBy('r.id', 'ASC')
->getQuery()
->getResult();
} else {
// Default: only approved documents
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->createQueryBuilder('r')
->join('r.doc', 'd')
->where('r.person = :person')
->andWhere('r.year = :year')
->andWhere('d.bid = :bid')
->andWhere('d.isApproved = :isApproved')
->setParameter('person', $person)
->setParameter('year', $acc['year'])
->setParameter('bid', $acc['bid'])
->setParameter('isApproved', true)
->orderBy('r.id', 'ASC')
->getQuery()
->getResult();
}
} elseif ($params['type'] == 'bank') {
$bank = $em->getRepository(\App\Entity\BankAccount::class)->findOneBy([
'bid' => $acc['bid'],
@ -63,11 +90,38 @@ class AccountingDocService
]);
if (!$bank)
return ['error' => 'بانک یافت نشد'];
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'bank' => $bank,
], [
'id' => 'DESC'
]);
// Check if we should include preview documents
$includePreview = $params['includePreview'] ?? false;
if ($includePreview) {
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->createQueryBuilder('r')
->join('r.doc', 'd')
->where('r.bank = :bank')
->andWhere('r.year = :year')
->andWhere('d.bid = :bid')
->setParameter('bank', $bank)
->setParameter('year', $acc['year'])
->setParameter('bid', $acc['bid'])
->orderBy('r.id', 'ASC')
->getQuery()
->getResult();
} else {
// Default: only approved documents
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->createQueryBuilder('r')
->join('r.doc', 'd')
->where('r.bank = :bank')
->andWhere('r.year = :year')
->andWhere('d.bid = :bid')
->andWhere('d.isApproved = :isApproved')
->setParameter('bank', $bank)
->setParameter('year', $acc['year'])
->setParameter('bid', $acc['bid'])
->setParameter('isApproved', true)
->orderBy('r.id', 'ASC')
->getQuery()
->getResult();
}
} elseif ($params['type'] == 'cashdesk') {
$cashdesk = $em->getRepository(\App\Entity\Cashdesk::class)->findOneBy([
'bid' => $acc['bid'],
@ -75,11 +129,38 @@ class AccountingDocService
]);
if (!$cashdesk)
return ['error' => 'صندوق یافت نشد'];
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'cashdesk' => $cashdesk,
], [
'id' => 'DESC'
]);
// Check if we should include preview documents
$includePreview = $params['includePreview'] ?? false;
if ($includePreview) {
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->createQueryBuilder('r')
->join('r.doc', 'd')
->where('r.cashdesk = :cashdesk')
->andWhere('r.year = :year')
->andWhere('d.bid = :bid')
->setParameter('cashdesk', $cashdesk)
->setParameter('year', $acc['year'])
->setParameter('bid', $acc['bid'])
->orderBy('r.id', 'ASC')
->getQuery()
->getResult();
} else {
// Default: only approved documents
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->createQueryBuilder('r')
->join('r.doc', 'd')
->where('r.cashdesk = :cashdesk')
->andWhere('r.year = :year')
->andWhere('d.bid = :bid')
->andWhere('d.isApproved = :isApproved')
->setParameter('cashdesk', $cashdesk)
->setParameter('year', $acc['year'])
->setParameter('bid', $acc['bid'])
->setParameter('isApproved', true)
->orderBy('r.id', 'ASC')
->getQuery()
->getResult();
}
} elseif ($params['type'] == 'salary') {
$salary = $em->getRepository(\App\Entity\Salary::class)->findOneBy([
'bid' => $acc['bid'],
@ -87,16 +168,59 @@ class AccountingDocService
]);
if (!$salary)
return ['error' => 'حقوق یافت نشد'];
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'salary' => $salary,
], [
'id' => 'DESC'
]);
// Check if we should include preview documents
$includePreview = $params['includePreview'] ?? false;
if ($includePreview) {
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->createQueryBuilder('r')
->join('r.doc', 'd')
->where('r.salary = :salary')
->andWhere('r.year = :year')
->andWhere('d.bid = :bid')
->setParameter('salary', $salary)
->setParameter('year', $acc['year'])
->setParameter('bid', $acc['bid'])
->orderBy('r.id', 'ASC')
->getQuery()
->getResult();
} else {
// Default: only approved documents
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->createQueryBuilder('r')
->join('r.doc', 'd')
->where('r.salary = :salary')
->andWhere('r.year = :year')
->andWhere('d.bid = :bid')
->andWhere('d.isApproved = :isApproved')
->setParameter('salary', $salary)
->setParameter('year', $acc['year'])
->setParameter('bid', $acc['bid'])
->setParameter('isApproved', true)
->orderBy('r.id', 'ASC')
->getQuery()
->getResult();
}
} else {
return ['error' => 'نوع پشتیبانی نمی‌شود'];
}
$dataTemp = [];
$runningBalance = 0; // باقی‌مانده تجمعی
foreach ($data as $item) {
// محاسبه باقی‌مانده تجمعی
$runningBalance += ($item->getBs() - $item->getBd());
// محاسبه تشخیص بر اساس باقی‌مانده تجمعی
$settlement = '';
if ($runningBalance > 0) {
$settlement = 'بستانکار';
} elseif ($runningBalance < 0) {
$settlement = 'بدهکار';
} else {
$settlement = 'تسویه‌شده';
}
$temp = [
'id' => $item->getId(),
'dateSubmit' => $item->getDoc()->getDateSubmit(),
@ -107,10 +231,14 @@ class AccountingDocService
'bs' => $item->getBs(),
'bd' => $item->getBd(),
'code' => $item->getDoc()->getCode(),
'submitter' => $item->getDoc()->getSubmitter()->getFullName()
'submitter' => $item->getDoc()->getSubmitter()->getFullName(),
'settlement' => $settlement, // ستون تشخیص
'balance' => $runningBalance // ستون باقی‌مانده
];
$dataTemp[] = $temp;
}
return $dataTemp;
// معکوس کردن ترتیب برای نمایش جدیدترین‌ها اول
return array_reverse($dataTemp);
}
}

View file

@ -95,7 +95,7 @@ class PersonService
if (!empty($search) || $search === '0') {
$search = trim($search);
$queryBuilder->andWhere('p.nikename LIKE :search OR p.name LIKE :search OR p.code LIKE :search OR p.mobile LIKE :search')
$queryBuilder->andWhere('p.nikename LIKE :search OR p.name LIKE :search OR p.code LIKE :search OR p.mobile LIKE :search OR p.paymentId LIKE :search')
->setParameter('search', "%$search%");
}
@ -288,6 +288,22 @@ class PersonService
if (isset($params['shenasemeli'])) $person->setShenasemeli($params['shenasemeli']);
if (isset($params['company'])) $person->setCompany($params['company']);
if (isset($params['tags'])) $person->setTags($params['tags']);
// بررسی منحصر به فرد بودن شناسه پرداخت در کسب و کار
if (isset($params['paymentId']) && !empty(trim($params['paymentId']))) {
$existingPerson = $em->getRepository(\App\Entity\Person::class)->findOneBy([
'paymentId' => trim($params['paymentId']),
'bid' => $acc['bid']
]);
// اگر شخص دیگری با همین شناسه پرداخت وجود دارد و این شخص فعلی نیست
if ($existingPerson && $existingPerson->getId() !== $person->getId()) {
return ['result' => -4, 'error' => 'شناسه پرداخت تکراری است'];
}
$person->setPaymentId(trim($params['paymentId']));
} else {
$person->setPaymentId(null);
}
if (array_key_exists('prelabel', $params)) {
if ($params['prelabel'] != '') {

View file

@ -0,0 +1,64 @@
<?php
namespace App\Command;
use App\Service\OAuthService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:cleanup-oauth',
description: 'پاکسازی توکن‌ها و کدهای منقضی شده OAuth',
)]
class CleanupOAuthCommand extends Command
{
private OAuthService $oauthService;
public function __construct(OAuthService $oauthService)
{
parent::__construct();
$this->oauthService = $oauthService;
}
protected function configure(): void
{
$this
->setHelp('این دستور توکن‌ها و کدهای منقضی شده OAuth را پاکسازی می‌کند.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('پاکسازی OAuth');
try {
$result = $this->oauthService->cleanupExpiredItems();
$io->success('پاکسازی OAuth با موفقیت انجام شد.');
$io->table(
['نوع', 'تعداد حذف شده'],
[
['کدهای مجوز منقضی شده', $result['expired_codes']],
['توکن‌های منقضی شده', $result['expired_tokens']]
]
);
$total = $result['expired_codes'] + $result['expired_tokens'];
if ($total > 0) {
$io->info("در مجموع {$total} آیتم منقضی شده پاکسازی شد.");
} else {
$io->info('هیچ آیتم منقضی شده‌ای یافت نشد.');
}
return Command::SUCCESS;
} catch (\Exception $e) {
$io->error('خطا در پاکسازی OAuth: ' . $e->getMessage());
return Command::FAILURE;
}
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace App\Command;
use App\Service\OAuthService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:create-oauth-scopes',
description: 'ایجاد محدوده‌های پیش‌فرض OAuth',
)]
class CreateOAuthScopesCommand extends Command
{
private OAuthService $oauthService;
public function __construct(OAuthService $oauthService)
{
parent::__construct();
$this->oauthService = $oauthService;
}
protected function configure(): void
{
$this
->setHelp('این دستور محدوده‌های پیش‌فرض OAuth را ایجاد می‌کند.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('ایجاد محدوده‌های پیش‌فرض OAuth');
try {
$this->oauthService->createDefaultScopes();
$io->success('محدوده‌های پیش‌فرض OAuth با موفقیت ایجاد شدند.');
$io->table(
['نام محدوده', 'توضیحات', 'پیش‌فرض'],
[
['read_profile', 'دسترسی به اطلاعات پروفایل کاربر', 'بله'],
['write_profile', 'ویرایش اطلاعات پروفایل کاربر', 'خیر'],
['read_business', 'دسترسی به اطلاعات کسب‌وکار', 'بله'],
['write_business', 'ویرایش اطلاعات کسب‌وکار', 'خیر'],
['read_accounting', 'دسترسی به اطلاعات حسابداری', 'خیر'],
['write_accounting', 'ویرایش اطلاعات حسابداری', 'خیر'],
['read_reports', 'دسترسی به گزارش‌ها', 'خیر'],
['write_reports', 'ایجاد و ویرایش گزارش‌ها', 'خیر'],
['admin', 'دسترسی مدیریتی کامل', 'خیر']
]
);
return Command::SUCCESS;
} catch (\Exception $e) {
$io->error('خطا در ایجاد محدوده‌های OAuth: ' . $e->getMessage());
return Command::FAILURE;
}
}
}

View file

@ -272,8 +272,88 @@ class UpdateSoftwareCommand extends Command
}
}
/**
* Helper method to fix Git "dubious ownership" error
*/
private function fixGitOwnershipIssue(string $gitRoot): bool
{
try {
// Check if the directory is a Git repository
if (!is_dir($gitRoot . '/.git')) {
return false;
}
// Always add the directory to safe.directory when this method is called
// This handles cases where Git detects dubious ownership for other reasons
$safeDirProcess = new Process(['git', 'config', '--global', '--add', 'safe.directory', $gitRoot], $gitRoot);
$safeDirProcess->setTimeout(300);
$safeDirProcess->run();
if ($safeDirProcess->isSuccessful()) {
$this->logger->info("Fixed Git ownership issue for directory: $gitRoot");
return true;
}
return false;
} catch (\Exception $e) {
$this->logger->warning("Failed to fix Git ownership issue: " . $e->getMessage());
return false;
}
}
/**
* Helper method to run Git command with ownership fix
*/
private function runGitCommand(array $command, string $workingDir, OutputInterface $output, int $retries = 3): void
{
$attempt = 0;
while ($attempt < $retries) {
try {
$process = new Process($command, $workingDir);
$process->setTimeout(3600);
if ($output->isVerbose()) {
$process->mustRun(function ($type, $buffer) use ($output) {
$this->writeOutput($output, $buffer);
});
} else {
$process->mustRun();
$this->writeOutput($output, $process->getOutput());
}
$this->logger->info('Git command executed successfully: ' . implode(' ', $command));
return;
} catch (ProcessFailedException $e) {
$attempt++;
$errorMessage = $e->getProcess()->getErrorOutput() ?: $e->getMessage();
$this->logger->warning("Attempt $attempt failed for " . implode(' ', $command) . ": $errorMessage");
$this->writeOutput($output, "<comment>Attempt $attempt failed: $errorMessage</comment>");
// If the command failed with "dubious ownership" error, try to fix it
if (str_contains($errorMessage, 'dubious ownership')) {
$this->writeOutput($output, "<comment>Detected Git ownership issue, attempting to fix...</comment>");
if ($this->fixGitOwnershipIssue($workingDir)) {
$this->writeOutput($output, "<info>Git ownership issue fixed, retrying command...</info>");
continue; // Retry the command without incrementing attempt
}
}
if ($attempt === $retries) {
throw new \RuntimeException('Command "' . implode(' ', $command) . '" failed after ' . $retries . ' attempts: ' . $errorMessage);
}
sleep(5);
}
}
}
private function runProcess(array $command, string $workingDir, OutputInterface $output, int $retries = 3, bool $isComposer = false): void
{
// If this is a Git command, use the specialized Git command runner
if (in_array($command[0], ['git'])) {
$this->runGitCommand($command, $workingDir, $output, $retries);
return;
}
$attempt = 0;
while ($attempt < $retries) {
try {
@ -284,6 +364,11 @@ class UpdateSoftwareCommand extends Command
'HOME' => '/var/www',
'COMPOSER_HOME' => '/var/www/.composer',
];
} elseif (in_array($command[0], ['npm', 'node'])) {
$env = [
'PATH' => '/usr/local/bin:/usr/bin:/bin:' . getenv('PATH'),
'HOME' => '/var/www',
];
}
$process = new Process($command, $workingDir, $env);
@ -318,13 +403,28 @@ class UpdateSoftwareCommand extends Command
private function getCurrentGitHead(): string
{
$process = new Process(['git', 'rev-parse', 'HEAD'], $this->rootDir);
$process->run();
if (!$process->isSuccessful()) {
$this->logger->warning('Failed to get current Git HEAD: ' . $process->getErrorOutput());
try {
$process = new Process(['git', 'rev-parse', 'HEAD'], $this->rootDir);
$process->run();
// If the command failed with "dubious ownership" error, try to fix it
if (!$process->isSuccessful() && str_contains($process->getErrorOutput(), 'dubious ownership')) {
if ($this->fixGitOwnershipIssue($this->rootDir)) {
// Retry the command after fixing ownership
$process = new Process(['git', 'rev-parse', 'HEAD'], $this->rootDir);
$process->run();
}
}
if (!$process->isSuccessful()) {
$this->logger->warning('Failed to get current Git HEAD: ' . $process->getErrorOutput());
return 'unknown';
}
return trim($process->getOutput());
} catch (\Exception $e) {
$this->logger->warning('Failed to get current Git HEAD: ' . $e->getMessage());
return 'unknown';
}
return trim($process->getOutput());
}
private function isUpToDate(): bool
@ -333,6 +433,16 @@ class UpdateSoftwareCommand extends Command
$this->runProcess(['git', 'fetch', 'origin'], $this->rootDir, new \Symfony\Component\Console\Output\NullOutput());
$process = new Process(['git', 'status', '-uno'], $this->rootDir);
$process->run();
// If the command failed with "dubious ownership" error, try to fix it
if (!$process->isSuccessful() && str_contains($process->getErrorOutput(), 'dubious ownership')) {
if ($this->fixGitOwnershipIssue($this->rootDir)) {
// Retry the command after fixing ownership
$process = new Process(['git', 'status', '-uno'], $this->rootDir);
$process->run();
}
}
$status = $process->getOutput();
return strpos($status, 'Your branch is up to date') !== false;
} catch (\Exception $e) {
@ -548,7 +658,19 @@ class UpdateSoftwareCommand extends Command
$this->runProcess(['git', '--version'], $this->rootDir, $output, 1);
$this->runProcess(['composer', '--version'], $this->rootDir, $output, 1, true);
$this->runProcess(['php', '-v'], $this->rootDir, $output, 1);
$this->runProcess(['npm', '--version'], $this->rootDir, $output, 1);
// Check npm with proper PATH
try {
$env = ['PATH' => '/usr/local/bin:/usr/bin:/bin:' . getenv('PATH')];
$process = new Process(['npm', '--version'], $this->rootDir, $env);
$process->setTimeout(30);
$process->mustRun();
$this->logger->info('Command executed successfully: npm --version');
$this->writeOutput($output, $process->getOutput());
} catch (ProcessFailedException $e) {
$this->logger->warning("Attempt 1 failed for npm --version: " . $e->getProcess()->getErrorOutput());
$this->writeOutput($output, "<comment>Warning: npm not found or not accessible. Frontend build may fail.</comment>");
}
$process = new Process(['whoami'], $this->rootDir);
$process->run();
@ -558,6 +680,16 @@ class UpdateSoftwareCommand extends Command
$this->logger->warning('Command executed as root user.');
}
// Check and fix Git ownership issues proactively
if (is_dir($this->rootDir . '/.git')) {
$this->writeOutput($output, 'Checking Git repository ownership...');
if ($this->fixGitOwnershipIssue($this->rootDir)) {
$this->writeOutput($output, '<info>Git ownership issue detected and fixed.</info>');
} else {
$this->writeOutput($output, '<info>Git repository ownership is correct.</info>');
}
}
$this->writeOutput($output, 'Pre-update checks completed successfully.');
}

View file

@ -19,6 +19,7 @@ use App\Entity\WalletTransaction;
use App\Service\Extractor;
use App\Service\Jdate;
use App\Service\JsonResp;
use App\Service\Log;
use App\Service\Notification;
use App\Service\Provider;
use App\Service\registryMGR;
@ -359,6 +360,9 @@ class AdminController extends AbstractController
'passChequeInput' => $registryMGR->get('sms', 'plugAccproPassChequeInput'),
'rejectChequeInput' => $registryMGR->get('sms', 'plugAccproRejectChequeInput')
];
$resp['plugWarranty'] = [
'sendSerial' => $registryMGR->get('sms', 'plugWarrantySendSerial'),
];
return $this->json($resp);
}
@ -430,7 +434,10 @@ class AdminController extends AbstractController
if (array_key_exists('rejectChequeInput', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproRejectChequeInput', $params['plugAccpro']['rejectChequeInput'] ?? '');
}
if (array_key_exists('plugWarranty', $params)) {
if (array_key_exists('sendSerial', $params['plugWarranty']))
$registryMGR->update('sms', 'plugWarrantySendSerial', $params['plugWarranty']['sendSerial'] ?? '');
}
return $this->json(JsonResp::success());
}
@ -603,12 +610,16 @@ class AdminController extends AbstractController
}
$temp['totalPays'] = $totalPays;
$walletIncomes = $entityManager->getRepository(WalletTransaction::class)->findAllIncome($bid);
$totalIcome = 0;
foreach ($walletIncomes as $walletIncome) {
$totalIcome += $walletIncome->getAmount();
// محاسبه درآمد از تراکنش‌های sell
$walletSells = $entityManager->getRepository(WalletTransaction::class)->findBy(['bid' => $bid, 'type' => 'sell']);
$totalIncome = 0;
foreach ($walletSells as $walletSell) {
$totalIncome += (float) $walletSell->getAmount();
}
$temp['totalIncome'] = $totalIcome;
$temp['totalIncome'] = $totalIncome;
// محاسبه موجودی (درآمد - هزینه)
$temp['walletBalance'] = $totalIncome - $totalPays;
$temp['id'] = $bid->getId();
$temp['bidName'] = $bid->getName();
@ -724,4 +735,457 @@ class AdminController extends AbstractController
return $this->json($res);
}
#[Route('/api/admin/business/charge/add', name: 'admin_business_charge_add', methods: ['POST'])]
public function admin_business_charge_add(
Request $request,
EntityManagerInterface $entityManager,
Log $logService,
Jdate $jdate
): JsonResponse {
$params = json_decode($request->getContent(), true);
if (!isset($params['businessId']) || !isset($params['amount']) || !isset($params['description'])) {
return $this->json(['success' => false, 'message' => 'تمام فیلدهای ضروری را وارد کنید']);
}
$business = $entityManager->getRepository(Business::class)->find($params['businessId']);
if (!$business) {
return $this->json(['success' => false, 'message' => 'کسب و کار یافت نشد']);
}
$currentCharge = (float) ($business->getSmsCharge() ?? 0);
$newAmount = (float) $params['amount'];
$newCharge = $currentCharge + $newAmount;
$business->setSmsCharge((string) $newCharge);
$entityManager->persist($business);
$entityManager->flush();
// ثبت لاگ
$logService->insert(
'مدیریت اعتبار',
"افزایش اعتبار پیامک به مبلغ {$newAmount} ریال. اعتبار قبلی: {$currentCharge} ریال، اعتبار جدید: {$newCharge} ریال. توضیحات: {$params['description']}",
$this->getUser(),
$business
);
return $this->json([
'success' => true,
'message' => 'اعتبار با موفقیت افزایش یافت',
'data' => [
'previousCharge' => $currentCharge,
'newCharge' => $newCharge,
'addedAmount' => $newAmount
]
]);
}
#[Route('/api/admin/business/plugin/activate', name: 'admin_business_plugin_activate', methods: ['POST'])]
public function admin_business_plugin_activate(
Request $request,
EntityManagerInterface $entityManager,
Log $logService
): JsonResponse {
$params = json_decode($request->getContent(), true);
if (!isset($params['businessId']) || !isset($params['pluginCode']) || !isset($params['duration'])) {
return $this->json(['success' => false, 'message' => 'تمام فیلدهای ضروری را وارد کنید']);
}
$business = $entityManager->getRepository(Business::class)->find($params['businessId']);
if (!$business) {
return $this->json(['success' => false, 'message' => 'کسب و کار یافت نشد']);
}
$pluginProduct = $entityManager->getRepository(\App\Entity\PluginProdect::class)->findOneBy(['code' => $params['pluginCode']]);
if (!$pluginProduct) {
return $this->json(['success' => false, 'message' => 'افزونه یافت نشد']);
}
// بررسی اینکه آیا افزونه قبلاً فعال شده یا خیر
$existingPlugin = $entityManager->getRepository(\App\Entity\Plugin::class)->findOneBy([
'bid' => $business,
'name' => $params['pluginCode']
]);
$currentTime = time();
$expireTime = $currentTime + ($params['duration'] * 86400); // تبدیل روز به ثانیه
if ($existingPlugin) {
// اگر افزونه قبلاً فعال بوده، تاریخ انقضا را تمدید کن
$oldExpire = $existingPlugin->getDateExpire();
$existingPlugin->setDateExpire((string) $expireTime);
$existingPlugin->setStatus('100');
$entityManager->persist($existingPlugin);
} else {
// ایجاد افزونه جدید
$plugin = new \App\Entity\Plugin();
$plugin->setBid($business);
$plugin->setName($params['pluginCode']);
$plugin->setDateSubmit((string) $currentTime);
$plugin->setDateExpire((string) $expireTime);
$plugin->setStatus('100');
$plugin->setSubmitter($this->getUser());
$plugin->setPrice('0'); // رایگان برای ادمین
$plugin->setDes($params['description'] ?? 'فعال‌سازی توسط ادمین');
$entityManager->persist($plugin);
}
$entityManager->flush();
// ثبت لاگ
$durationText = $params['duration'] . ' روز';
$logService->insert(
'مدیریت افزونه',
"فعال‌سازی افزونه {$pluginProduct->getName()} برای مدت {$durationText}. توضیحات: " . (isset($params['description']) ? $params['description'] : 'فعال‌سازی توسط ادمین'),
$this->getUser(),
$business
);
return $this->json([
'success' => true,
'message' => 'افزونه با موفقیت فعال شد',
'data' => [
'pluginName' => $pluginProduct->getName(),
'expireDate' => date('Y-m-d H:i:s', $expireTime),
'duration' => $params['duration']
]
]);
}
#[Route('/api/admin/business/report/{id}', name: 'admin_business_report', methods: ['GET'])]
public function admin_business_report(
string $id,
EntityManagerInterface $entityManager,
Jdate $jdate
): JsonResponse {
$business = $entityManager->getRepository(Business::class)->find($id);
if (!$business) {
return $this->json(['success' => false, 'message' => 'کسب و کار یافت نشد']);
}
// آمار اشخاص
$personsCount = count($entityManager->getRepository(\App\Entity\Person::class)->findBy(['bid' => $business]));
// آمار کالا و خدمات
$commodityCount = count($entityManager->getRepository(\App\Entity\Commodity::class)->findBy(['bid' => $business]));
// آمار اسناد حسابداری
$hesabdariDocsCount = count($entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findBy(['bid' => $business]));
// آمار اسناد انبار
$storeroomDocsCount = count($entityManager->getRepository(\App\Entity\StoreroomTicket::class)->findBy(['bid' => $business]));
// آمار بانک‌ها
$bankAccountsCount = count($entityManager->getRepository(\App\Entity\BankAccount::class)->findBy(['bid' => $business]));
// آمار سال‌های مالی
$yearsCount = count($entityManager->getRepository(\App\Entity\Year::class)->findBy(['bid' => $business]));
// آمار افزونه‌های فعال
$activePlugins = $entityManager->getRepository(\App\Entity\Plugin::class)->findBy([
'bid' => $business,
'status' => '100'
]);
$activePluginsCount = count($activePlugins);
// لیست افزونه‌های فعال
$activePluginsList = [];
foreach ($activePlugins as $plugin) {
$pluginProduct = $entityManager->getRepository(\App\Entity\PluginProdect::class)->findOneBy(['code' => $plugin->getName()]);
$activePluginsList[] = [
'name' => $pluginProduct ? $pluginProduct->getName() : $plugin->getName(),
'expireDate' => $jdate->jdate('Y/n/d H:i', $plugin->getDateExpire()),
'isExpired' => $plugin->getDateExpire() < time()
];
}
// محاسبه فضای آرشیو
$archiveFiles = $entityManager->getRepository(\App\Entity\ArchiveFile::class)->findBy(['bid' => $business]);
$totalArchiveSize = 0;
foreach ($archiveFiles as $file) {
$totalArchiveSize += (int) ($file->getFileSize() ? $file->getFileSize() : 0);
}
// آمار کیف پول
$walletTransactions = $entityManager->getRepository(\App\Entity\WalletTransaction::class)->findBy(['bid' => $business]);
$walletIncome = 0;
$walletExpense = 0;
foreach ($walletTransactions as $transaction) {
if ($transaction->getType() === 'sell') {
$walletIncome += (float) $transaction->getAmount();
} elseif ($transaction->getType() === 'pay') {
$walletExpense += (float) $transaction->getAmount();
}
}
$report = [
'businessInfo' => [
'id' => $business->getId(),
'name' => $business->getName(),
'legalName' => $business->getLegalName(),
'owner' => $business->getOwner()->getFullName(),
'ownerMobile' => $business->getOwner()->getMobile(),
'ownerEmail' => $business->getOwner()->getEmail(),
'dateRegister' => $jdate->jdate('Y/n/d H:i', $business->getDateSubmit()),
'field' => $business->getField(),
'type' => $business->getType(),
'address' => $business->getAddress(),
'tel' => $business->getTel(),
'mobile' => $business->getMobile(),
'email' => $business->getEmail(),
'website' => $business->getWesite(),
'shenasemeli' => $business->getShenasemeli(),
'codeeghtesadi' => $business->getCodeeghtesadi(),
'shomaresabt' => $business->getShomaresabt(),
'country' => $business->getCountry(),
'ostan' => $business->getOstan(),
'shahrestan' => $business->getShahrestan(),
'postalcode' => $business->getPostalcode(),
'maliyatafzode' => $business->getMaliyatafzode(),
'avatar' => $business->getAvatar(),
'sealFile' => $business->getSealFile(),
],
'statistics' => [
'personsCount' => $personsCount,
'commodityCount' => $commodityCount,
'hesabdariDocsCount' => $hesabdariDocsCount,
'storeroomDocsCount' => $storeroomDocsCount,
'bankAccountsCount' => $bankAccountsCount,
'yearsCount' => $yearsCount,
'activePluginsCount' => $activePluginsCount,
],
'financial' => [
'smsCharge' => (float) ($business->getSmsCharge() ?? 0),
'walletEnabled' => $business->isWalletEnable(),
'walletIncome' => $walletIncome,
'walletExpense' => $walletExpense,
'walletBalance' => $walletIncome - $walletExpense,
],
'storage' => [
'archiveSize' => $business->getArchiveSize(),
'totalArchiveSize' => $totalArchiveSize,
'archiveFilesCount' => count($archiveFiles),
],
'plugins' => [
'activeCount' => $activePluginsCount,
'activeList' => $activePluginsList,
],
'features' => [
'storeOnline' => $business->isStoreOnline(),
'shortlinks' => $business->isShortlinks(),
'walletEnable' => $business->isWalletEnable(),
'commodityUpdateSellPriceAuto' => $business->isCommodityUpdateSellPriceAuto(),
'commodityUpdateBuyPriceAuto' => $business->isCommodityUpdateBuyPriceAuto(),
'profitCalcType' => $business->getProfitCalcType(),
]
];
return $this->json([
'success' => true,
'data' => $report
]);
}
#[Route('/api/admin/business/wallet/balance/{id}', name: 'admin_business_wallet_balance', methods: ['GET'])]
public function admin_business_wallet_balance(
string $id,
EntityManagerInterface $entityManager,
Jdate $jdate
): JsonResponse {
$business = $entityManager->getRepository(Business::class)->find($id);
if (!$business) {
return $this->json(['success' => false, 'message' => 'کسب و کار یافت نشد']);
}
if (!$business->isWalletEnable()) {
return $this->json(['success' => false, 'message' => 'کیف پول برای این کسب و کار فعال نیست']);
}
// محاسبه موجودی با استفاده از repository
$walletBalance = $entityManager->getRepository(\App\Entity\WalletTransaction::class)->calculateWalletBalance($business);
// محاسبه درآمد و هزینه جداگانه
$walletSells = $entityManager->getRepository(\App\Entity\WalletTransaction::class)->findBy(['bid' => $business, 'type' => 'sell']);
$walletPays = $entityManager->getRepository(\App\Entity\WalletTransaction::class)->findBy(['bid' => $business, 'type' => 'pay']);
$totalIncome = 0;
foreach ($walletSells as $sell) {
$totalIncome += (float) $sell->getAmount();
}
$totalExpense = 0;
foreach ($walletPays as $pay) {
$totalExpense += (float) $pay->getAmount();
}
return $this->json([
'success' => true,
'data' => [
'businessId' => $business->getId(),
'businessName' => $business->getName(),
'walletBalance' => $walletBalance,
'totalIncome' => $totalIncome,
'totalExpense' => $totalExpense,
'transactionsCount' => [
'sell' => count($walletSells),
'pay' => count($walletPays)
],
'lastTransactions' => [
'sells' => array_slice(array_map(function($sell) use ($jdate) {
return [
'id' => $sell->getId(),
'amount' => (float) $sell->getAmount(),
'date' => $jdate->jdate('Y/n/d H:i', $sell->getDateSubmit()),
'description' => $sell->getDes()
];
}, $walletSells), 0, 5),
'pays' => array_slice(array_map(function($pay) use ($jdate) {
return [
'id' => $pay->getId(),
'amount' => (float) $pay->getAmount(),
'date' => $jdate->jdate('Y/n/d H:i', $pay->getDateSubmit()),
'description' => $pay->getDes(),
'refID' => $pay->getRefID()
];
}, $walletPays), 0, 5)
]
]
]);
}
#[Route('/api/admin/business/wallet/transactions/{id}', name: 'admin_business_wallet_transactions', methods: ['GET'])]
public function admin_business_wallet_transactions(
string $id,
EntityManagerInterface $entityManager,
Jdate $jdate,
Request $request
): JsonResponse {
$business = $entityManager->getRepository(Business::class)->find($id);
if (!$business) {
return $this->json(['success' => false, 'message' => 'کسب و کار یافت نشد']);
}
if (!$business->isWalletEnable()) {
return $this->json(['success' => false, 'message' => 'کیف پول برای این کسب و کار فعال نیست']);
}
// پارامترهای صفحه‌بندی
$page = max(1, (int) ($request->query->get('page', 1)));
$limit = max(1, min(100, (int) ($request->query->get('limit', 20))));
$offset = ($page - 1) * $limit;
// فیلتر نوع تراکنش
$type = $request->query->get('type'); // 'sell' یا 'pay' یا null برای همه
$qb = $entityManager->createQueryBuilder();
$qb->select('w')
->from(\App\Entity\WalletTransaction::class, 'w')
->where('w.bid = :business')
->setParameter('business', $business)
->orderBy('w.dateSubmit', 'DESC');
if ($type && in_array($type, ['sell', 'pay'])) {
$qb->andWhere('w.type = :type')
->setParameter('type', $type);
}
// محاسبه تعداد کل
$countQb = clone $qb;
$totalCount = $countQb->select('COUNT(w.id)')->getQuery()->getSingleScalarResult();
// اعمال صفحه‌بندی
$qb->setFirstResult($offset)
->setMaxResults($limit);
$transactions = $qb->getQuery()->getResult();
$transactionsData = [];
foreach ($transactions as $transaction) {
$transactionsData[] = [
'id' => $transaction->getId(),
'type' => $transaction->getType(),
'amount' => (float) $transaction->getAmount(),
'date' => $jdate->jdate('Y/n/d H:i', $transaction->getDateSubmit()),
'description' => $transaction->getDes(),
'refID' => $transaction->getRefID(),
'shaba' => $transaction->getShaba(),
'cardPan' => $transaction->getCardPan(),
'gatePay' => $transaction->getGatePay(),
'bank' => $transaction->getBank(),
'submitter' => $transaction->getSubmitter() ? $transaction->getSubmitter()->getFullName() : null
];
}
return $this->json([
'success' => true,
'data' => [
'businessId' => $business->getId(),
'businessName' => $business->getName(),
'transactions' => $transactionsData,
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => (int) $totalCount,
'totalPages' => ceil($totalCount / $limit)
]
]
]);
}
#[Route('/api/admin/business/plugins/list/{id}', name: 'admin_business_plugins_list', methods: ['GET'])]
public function admin_business_plugins_list(
string $id,
EntityManagerInterface $entityManager,
Jdate $jdate
): JsonResponse {
$business = $entityManager->getRepository(Business::class)->find($id);
if (!$business) {
return $this->json(['success' => false, 'message' => 'کسب و کار یافت نشد']);
}
// دریافت همه افزونه‌های موجود
$allPlugins = $entityManager->getRepository(\App\Entity\PluginProdect::class)->findAll();
// دریافت افزونه‌های فعال این کسب و کار
$businessPlugins = $entityManager->getRepository(\App\Entity\Plugin::class)->findBy([
'bid' => $business,
'status' => '100'
]);
$businessPluginCodes = array_map(fn($p) => $p->getName(), $businessPlugins);
$pluginsList = [];
foreach ($allPlugins as $plugin) {
$isActive = in_array($plugin->getCode(), $businessPluginCodes);
$businessPlugin = null;
if ($isActive) {
$businessPlugin = $entityManager->getRepository(\App\Entity\Plugin::class)->findOneBy([
'bid' => $business,
'name' => $plugin->getCode(),
'status' => '100'
]);
}
$pluginsList[] = [
'id' => $plugin->getId(),
'name' => $plugin->getName(),
'code' => $plugin->getCode(),
'price' => $plugin->getPrice(),
'timeLabel' => $plugin->getTimelabel(),
'icon' => $plugin->getIcon(),
'defaultOn' => $plugin->isDefaultOn(),
'isActive' => $isActive,
'expireDate' => $businessPlugin ? $jdate->jdate('Y/n/d H:i', $businessPlugin->getDateExpire()) : null,
'isExpired' => $businessPlugin ? $businessPlugin->getDateExpire() < time() : false,
'status' => $businessPlugin ? $businessPlugin->getStatus() : null,
];
}
return $this->json([
'success' => true,
'data' => $pluginsList
]);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,885 @@
<?php
namespace App\Controller;
use App\Entity\Business;
use App\Entity\Permission;
use App\Entity\User;
use App\Service\Access;
use App\Service\Extractor;
use App\Service\PluginService;
use Doctrine\ORM\EntityManagerInterface;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
class BackupController extends AbstractController
{
#[Route('/api/backup/create', name: 'api_backup_create', methods: ['POST'])]
public function createBackup(
Request $request,
#[CurrentUser] ?User $user,
Access $access,
PluginService $pluginService,
EntityManagerInterface $entityManager
): Response {
// بررسی دسترسی settings
$acc = $access->hasRole('settings');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// بررسی فعال بودن افزونه accpro
if (!$pluginService->isActive('accpro', $acc['bid'])) {
return $this->json([
'result' => 0,
'message' => 'این قابلیت فقط برای کاربران افزونه accpro در دسترس است.'
]);
}
try {
// ایجاد فایل اکسل
$spreadsheet = new Spreadsheet();
// اطلاعات کسب و کار
$this->addBusinessInfoSheet($spreadsheet, $acc['bid'], $entityManager);
// اطلاعات اشخاص
$this->addPersonsSheet($spreadsheet, $acc['bid'], $entityManager);
// اطلاعات کالاها
$this->addCommoditiesSheet($spreadsheet, $acc['bid'], $entityManager);
// اطلاعات حساب‌های بانکی
$this->addBankAccountsSheet($spreadsheet, $acc['bid'], $entityManager);
// اطلاعات اسناد حسابداری
$this->addHesabdariDocsSheet($spreadsheet, $acc['bid'], $entityManager);
// اطلاعات فاکتورهای فروش
$this->addSellDocsSheet($spreadsheet, $acc['bid'], $entityManager);
// اطلاعات فاکتورهای خرید
$this->addBuyDocsSheet($spreadsheet, $acc['bid'], $entityManager);
// اطلاعات انبار
$this->addStoreroomSheet($spreadsheet, $acc['bid'], $entityManager);
// جدول حساب‌ها
$this->addHesabdariTableSheet($spreadsheet, $acc['bid'], $entityManager);
// تراکنش‌ها
$this->addHesabdariRowSheet($spreadsheet, $acc['bid'], $entityManager);
// دریافت از اشخاص
$this->addPersonReceiveSheet($spreadsheet, $acc['bid'], $entityManager);
// پرداخت به اشخاص
$this->addPersonSendSheet($spreadsheet, $acc['bid'], $entityManager);
// برگشت از خرید
$this->addRfBuySheet($spreadsheet, $acc['bid'], $entityManager);
// برگشت از فروش
$this->addRfSellSheet($spreadsheet, $acc['bid'], $entityManager);
// حذف sheet پیش‌فرض
$spreadsheet->removeSheetByIndex(0);
// ایجاد فایل
$writer = new Xlsx($spreadsheet);
$filename = 'backup_' . $acc['bid']->getName() . '_' . date('Y-m-d_H-i-s') . '.xlsx';
$filepath = sys_get_temp_dir() . '/' . $filename;
$writer->save($filepath);
// ارسال فایل
$response = $this->file($filepath, $filename, ResponseHeaderBag::DISPOSITION_ATTACHMENT);
// حذف فایل موقت بعد از ارسال response
$response->deleteFileAfterSend(true);
return $response;
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => 'خطا در ایجاد نسخه پشتیبان: ' . $e->getMessage()
]);
}
}
private function addBusinessInfoSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('اطلاعات کسب و کار');
// هدرها
$headers = [
'نام کسب و کار',
'نام قانونی',
'زمینه فعالیت',
'نوع فعالیت',
'شناسه ملی',
'کد اقتصادی',
'شماره ثبت',
'کشور',
'استان',
'شهر',
'کد پستی',
'تلفن',
'موبایل',
'آدرس',
'وب‌سایت',
'ایمیل',
'مالیات بر ارزش افزوده',
'تاریخ ثبت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// داده‌ها
$data = [
$business->getName(),
$business->getLegalName(),
$business->getField(),
$business->getType(),
$business->getShenasemeli(),
$business->getCodeeghtesadi(),
$business->getShomaresabt(),
$business->getCountry(),
$business->getOstan(),
$business->getShahrestan(),
$business->getPostalcode(),
$business->getTel(),
$business->getMobile(),
$business->getAddress(),
$business->getWesite(),
$business->getEmail(),
$business->getMaliyatafzode(),
date('Y-m-d H:i:s', $business->getDateSubmit())
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . '2', $value);
$col++;
}
}
private function addPersonsSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('اشخاص');
// هدرها
$headers = [
'نام',
'نام خانوادگی',
'کد',
'نوع',
'تلفن',
'موبایل',
'ایمیل',
'آدرس',
'کد ملی',
'کد اقتصادی',
'تاریخ ثبت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت اشخاص
$persons = $entityManager->getRepository(\App\Entity\Person::class)->findBy(['bid' => $business]);
$row = 2;
foreach ($persons as $person) {
$data = [
$person->getName(),
$person->getNikename(),
$person->getCode(),
$person->getType() ? $person->getType()->first() ? $person->getType()->first()->getLabel() : '' : '',
$person->getTel(),
$person->getMobile(),
$person->getEmail(),
$person->getAddress(),
$person->getShenasemeli(),
$person->getCodeeghtesadi(),
'' // Entity Person فیلد تاریخ ثبت ندارد
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
private function addCommoditiesSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('کالاها');
// هدرها
$headers = [
'نام کالا',
'کد',
'دسته‌بندی',
'واحد',
'قیمت خرید',
'قیمت فروش',
'موجودی',
'حداقل سفارش',
'توضیحات',
'تاریخ ثبت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت کالاها
$commodities = $entityManager->getRepository(\App\Entity\Commodity::class)->findBy(['bid' => $business]);
$row = 2;
foreach ($commodities as $commodity) {
$data = [
$commodity->getName(),
$commodity->getCode(),
$commodity->getCat() ? $commodity->getCat()->getName() : '',
$commodity->getUnit() ? $commodity->getUnit()->getName() : '',
$commodity->getPriceBuy(),
$commodity->getPriceSell(),
'', // Entity Commodity فیلد موجودی ندارد
$commodity->getMinOrderCount(), // حداقل سفارش
$commodity->getDes(),
'' // Entity Commodity فیلد تاریخ ثبت ندارد
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
private function addBankAccountsSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('حساب‌های بانکی');
// هدرها
$headers = [
'نام حساب',
'شماره حساب',
'شماره کارت',
'شماره شبا',
'نام صاحب حساب',
'موجودی',
'توضیحات',
'تاریخ ثبت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت حساب‌های بانکی
$bankAccounts = $entityManager->getRepository(\App\Entity\BankAccount::class)->findBy(['bid' => $business]);
$row = 2;
foreach ($bankAccounts as $account) {
$data = [
$account->getName(),
$account->getAccountNum(),
$account->getCardNum(),
$account->getShaba(),
$account->getOwner(),
$account->getBalance(),
$account->getDes(),
'' // Entity BankAccount فیلد تاریخ ثبت ندارد
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
private function addHesabdariDocsSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('اسناد حسابداری');
// هدرها
$headers = [
'شماره سند',
'تاریخ',
'سال مالی',
'نوع سند',
'شرح',
'مبلغ',
'حساب بدهکار',
'حساب بستانکار',
'وضعیت',
'تاریخ ثبت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت اسناد حسابداری
$docs = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findBy(['bid' => $business]);
$row = 2;
foreach ($docs as $doc) {
// دریافت ردیف‌های حسابداری این سند
$rows = $doc->getHesabdariRows();
$debitAccounts = [];
$creditAccounts = [];
foreach ($rows as $hesabdariRow) {
$accountName = $hesabdariRow->getRef() ? $hesabdariRow->getRef()->getName() . ' (' . $hesabdariRow->getRef()->getCode() . ')' : '';
if ($hesabdariRow->getBd() && $hesabdariRow->getBd() > 0) {
$debitAccounts[] = $accountName;
}
if ($hesabdariRow->getBs() && $hesabdariRow->getBs() > 0) {
$creditAccounts[] = $accountName;
}
}
$data = [
$doc->getCode(),
$doc->getDate(),
$doc->getYear() ? $doc->getYear()->getLabel() : '',
$doc->getType(),
$doc->getDes(),
$doc->getAmount(),
implode(', ', array_unique($debitAccounts)),
implode(', ', array_unique($creditAccounts)),
$doc->getStatus(),
$doc->getDateSubmit()
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
private function addSellDocsSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('فاکتورهای فروش');
// هدرها
$headers = [
'شماره فاکتور',
'تاریخ',
'سال مالی',
'مشتری',
'مبلغ کل',
'درصد مالیات',
'تخفیف',
'مبلغ نهایی',
'وضعیت',
'تاریخ ثبت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت فاکتورهای فروش
$docs = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findBy([
'bid' => $business,
'type' => 'sell'
]);
$row = 2;
foreach ($docs as $doc) {
$data = [
$doc->getCode(),
$doc->getDate(),
$doc->getYear() ? $doc->getYear()->getLabel() : '',
$doc->getSalesman() ? $doc->getSalesman()->getName() . ' ' . $doc->getSalesman()->getNikename() : '',
$doc->getAmount(),
$doc->getTaxPercent(),
'', // Entity فیلد تخفیف ندارد
$doc->getAmount(), // مبلغ نهایی همان مبلغ کل است
$doc->getStatus(),
$doc->getDateSubmit()
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
private function addBuyDocsSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('فاکتورهای خرید');
// هدرها
$headers = [
'شماره فاکتور',
'تاریخ',
'سال مالی',
'فروشنده',
'مبلغ کل',
'درصد مالیات',
'تخفیف',
'مبلغ نهایی',
'وضعیت',
'تاریخ ثبت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت فاکتورهای خرید
$docs = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findBy([
'bid' => $business,
'type' => 'buy'
]);
$row = 2;
foreach ($docs as $doc) {
$data = [
$doc->getCode(),
$doc->getDate(),
$doc->getYear() ? $doc->getYear()->getLabel() : '',
$doc->getSalesman() ? $doc->getSalesman()->getName() . ' ' . $doc->getSalesman()->getNikename() : '',
$doc->getAmount(),
$doc->getTaxPercent(),
'', // Entity فیلد تخفیف ندارد
$doc->getAmount(), // مبلغ نهایی همان مبلغ کل است
$doc->getStatus(),
$doc->getDateSubmit()
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
private function addStoreroomSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('انبار');
// هدرها
$headers = [
'نام انبار',
'شناسه',
'آدرس',
'مسئول',
'وضعیت',
'تاریخ ثبت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت انبارها
$storerooms = $entityManager->getRepository(\App\Entity\Storeroom::class)->findBy(['bid' => $business]);
$row = 2;
foreach ($storerooms as $storeroom) {
$data = [
$storeroom->getName(),
$storeroom->getId(),
$storeroom->getAdr(),
$storeroom->getManager(),
$storeroom->isActive() ? 'فعال' : 'غیرفعال',
'' // Entity Storeroom فیلد تاریخ ثبت ندارد
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
private function addHesabdariTableSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('جدول حساب‌ها');
// هدرها
$headers = [
'کد حساب',
'نام حساب',
'نوع حساب',
'حساب والد',
'نوع موجودیت',
'وضعیت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت حساب‌ها
$accounts = $entityManager->getRepository(\App\Entity\HesabdariTable::class)->findBy(['bid' => $business]);
$row = 2;
foreach ($accounts as $account) {
$data = [
$account->getCode(),
$account->getName(),
$account->getType(),
$account->getUpper() ? $account->getUpper()->getName() . ' (' . $account->getUpper()->getCode() . ')' : '',
$account->getEntity(),
$account->getUpper() ? 'زیرمجموعه' : 'حساب اصلی'
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
private function addHesabdariRowSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('تراکنش‌ها');
// هدرها
$headers = [
'شماره سند',
'تاریخ سند',
'سال مالی',
'نوع سند',
'حساب',
'کد حساب',
'بدهکار',
'بستانکار',
'شخص',
'حساب بانکی',
'کالا',
'تعداد کالا',
'توضیحات',
'مرجع',
'داده مرجع',
'تخفیف',
'مالیات',
'نوع تخفیف',
'درصد تخفیف'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت تمام تراکنش‌ها
$rows = $entityManager->getRepository(\App\Entity\HesabdariRow::class)->findBy(['bid' => $business]);
$row = 2;
foreach ($rows as $hesabdariRow) {
$doc = $hesabdariRow->getDoc();
$data = [
$doc ? $doc->getCode() : '',
$doc ? $doc->getDate() : '',
$hesabdariRow->getYear() ? $hesabdariRow->getYear()->getLabel() : '',
$doc ? $doc->getType() : '',
$hesabdariRow->getRef() ? $hesabdariRow->getRef()->getName() : '',
$hesabdariRow->getRef() ? $hesabdariRow->getRef()->getCode() : '',
$hesabdariRow->getBd() ?: '',
$hesabdariRow->getBs() ?: '',
$hesabdariRow->getPerson() ? $hesabdariRow->getPerson()->getName() . ' ' . $hesabdariRow->getPerson()->getNikename() : '',
$hesabdariRow->getBank() ? $hesabdariRow->getBank()->getName() : '',
$hesabdariRow->getCommodity() ? $hesabdariRow->getCommodity()->getName() : '',
$hesabdariRow->getCommdityCount() ?: '',
$hesabdariRow->getDes() ?: '',
$hesabdariRow->getReferral() ?: '',
$hesabdariRow->getRefData() ?: '',
$hesabdariRow->getDiscount() ?: '',
$hesabdariRow->getTax() ?: '',
$hesabdariRow->getDiscountType() ?: '',
$hesabdariRow->getDiscountPercent() ?: ''
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
private function addPersonReceiveSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('دریافت از اشخاص');
// هدرها
$headers = [
'شماره سند',
'تاریخ',
'سال مالی',
'شخص',
'مبلغ',
'واحد پول',
'توضیحات',
'وضعیت',
'تاریخ ثبت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت اسناد دریافت از اشخاص
$docs = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findBy([
'bid' => $business,
'type' => 'person_receive'
]);
$row = 2;
foreach ($docs as $doc) {
$data = [
$doc->getCode(),
$doc->getDate(),
$doc->getYear() ? $doc->getYear()->getLabel() : '',
$doc->getSalesman() ? $doc->getSalesman()->getName() . ' ' . $doc->getSalesman()->getNikename() : '',
$doc->getAmount(),
$doc->getMoney() ? $doc->getMoney()->getName() : '',
$doc->getDes(),
$doc->getStatus(),
$doc->getDateSubmit()
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
private function addPersonSendSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('پرداخت به اشخاص');
// هدرها
$headers = [
'شماره سند',
'تاریخ',
'سال مالی',
'شخص',
'مبلغ',
'واحد پول',
'توضیحات',
'وضعیت',
'تاریخ ثبت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت اسناد پرداخت به اشخاص
$docs = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findBy([
'bid' => $business,
'type' => 'person_send'
]);
$row = 2;
foreach ($docs as $doc) {
$data = [
$doc->getCode(),
$doc->getDate(),
$doc->getYear() ? $doc->getYear()->getLabel() : '',
$doc->getSalesman() ? $doc->getSalesman()->getName() . ' ' . $doc->getSalesman()->getNikename() : '',
$doc->getAmount(),
$doc->getMoney() ? $doc->getMoney()->getName() : '',
$doc->getDes(),
$doc->getStatus(),
$doc->getDateSubmit()
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
private function addRfBuySheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('برگشت از خرید');
// هدرها
$headers = [
'شماره فاکتور',
'تاریخ',
'سال مالی',
'فروشنده',
'مبلغ کل',
'درصد مالیات',
'تخفیف',
'مبلغ نهایی',
'وضعیت',
'تاریخ ثبت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت فاکتورهای برگشت از خرید
$docs = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findBy([
'bid' => $business,
'type' => 'rfbuy'
]);
$row = 2;
foreach ($docs as $doc) {
$data = [
$doc->getCode(),
$doc->getDate(),
$doc->getYear() ? $doc->getYear()->getLabel() : '',
$doc->getSalesman() ? $doc->getSalesman()->getName() . ' ' . $doc->getSalesman()->getNikename() : '',
$doc->getAmount(),
$doc->getTaxPercent(),
'', // Entity فیلد تخفیف ندارد
$doc->getAmount(), // مبلغ نهایی همان مبلغ کل است
$doc->getStatus(),
$doc->getDateSubmit()
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
private function addRfSellSheet(Spreadsheet $spreadsheet, Business $business, EntityManagerInterface $entityManager): void
{
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('برگشت از فروش');
// هدرها
$headers = [
'شماره فاکتور',
'تاریخ',
'سال مالی',
'مشتری',
'مبلغ کل',
'درصد مالیات',
'تخفیف',
'مبلغ نهایی',
'وضعیت',
'تاریخ ثبت'
];
$col = 'A';
foreach ($headers as $header) {
$sheet->setCellValue($col . '1', $header);
$col++;
}
// دریافت فاکتورهای برگشت از فروش
$docs = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findBy([
'bid' => $business,
'type' => 'rfsell'
]);
$row = 2;
foreach ($docs as $doc) {
$data = [
$doc->getCode(),
$doc->getDate(),
$doc->getYear() ? $doc->getYear()->getLabel() : '',
$doc->getSalesman() ? $doc->getSalesman()->getName() . ' ' . $doc->getSalesman()->getNikename() : '',
$doc->getAmount(),
$doc->getTaxPercent(),
'', // Entity فیلد تخفیف ندارد
$doc->getAmount(), // مبلغ نهایی همان مبلغ کل است
$doc->getStatus(),
$doc->getDateSubmit()
];
$col = 'A';
foreach ($data as $value) {
$sheet->setCellValue($col . $row, $value);
$col++;
}
$row++;
}
}
}

View file

@ -47,16 +47,33 @@ class BankController extends AbstractController
foreach ($datas as $data) {
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bank' => $data
]);
// Use query builder to filter by approved documents
$items = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bank = :bank')
->andWhere('d.isApproved = :isApproved')
->setParameter('bank', $data)
->setParameter('isApproved', true)
->getQuery()
->getResult();
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
}
$data->setBalance($bd - $bs);
}
return $this->json($provider->ArrayEntity2Array($datas, 0));
$result = [];
foreach ($datas as $data) {
$bankData = $provider->ArrayEntity2Array([$data], 0)[0];
if (isset($data->tempData)) {
$bankData['bs'] = $data->tempData['bs'];
$bankData['bd'] = $data->tempData['bd'];
}
$result[] = $bankData;
}
return $this->json($result);
}
#[Route('/api/bank/search', name: 'app_bank_search')]
@ -95,18 +112,40 @@ class BankController extends AbstractController
foreach ($datas as $data) {
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bank' => $data
]);
// Use query builder to filter by approved documents
$items = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bank = :bank')
->andWhere('d.isApproved = :isApproved')
->setParameter('bank', $data)
->setParameter('isApproved', true)
->getQuery()
->getResult();
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
$bs += (float) $item->getBs();
$bd += (float) $item->getBd();
}
$data->setBalance($bd - $bs);
// اضافه کردن مقادیر به array برای انتقال به frontend
$data->tempData = [
'bs' => $bs,
'bd' => $bd
];
}
$result = [];
foreach ($datas as $data) {
$bankData = $provider->ArrayEntity2Array([$data], 0)[0];
if (isset($data->tempData)) {
$bankData['bs'] = $data->tempData['bs'];
$bankData['bd'] = $data->tempData['bd'];
}
$result[] = $bankData;
}
return $this->json([
'items' => $provider->ArrayEntity2Array($datas, 0),
'items' => $result,
'total' => count($datas)
]);
}
@ -187,15 +226,15 @@ class BankController extends AbstractController
throw $this->createNotFoundException();
}
$data->setBid($acc['bid']);
$data->setname($params['name']);
$data->setDes($params['des']);
$data->setOwner($params['owner']);
$data->setAccountNum($params['accountNum']);
$data->setCardNum($params['cardNum']);
$data->setShaba($params['shaba']);
$data->setShobe($params['shobe']);
$data->setPosNum($params['posNum']);
$data->setMobileInternetBank($params['mobileInternetbank']);
$data->setName($params['name'] ?? '');
$data->setDes($params['des'] ?? null);
$data->setOwner($params['owner'] ?? null);
$data->setAccountNum($params['accountNum'] ?? null);
$data->setCardNum($params['cardNum'] ?? null);
$data->setShaba($params['shaba'] ?? null);
$data->setShobe($params['shobe'] ?? null);
$data->setPosNum($params['posNum'] ?? null);
$data->setMobileInternetBank($params['mobileInternetbank'] ?? null);
$entityManager->persist($data);
$entityManager->flush();
$log->insert('بانک', 'حساب بانکی با نام ' . $params['name'] . ' افزوده/ویرایش شد.', $this->getUser(), $request->headers->get('activeBid'));
@ -212,8 +251,17 @@ class BankController extends AbstractController
$bank = $entityManager->getRepository(BankAccount::class)->findOneBy(['bid' => $acc['bid'], 'code' => $code]);
if (!$bank)
throw $this->createNotFoundException();
//check accounting docs
$rows = $entityManager->getRepository(HesabdariRow::class)->findby(['bid' => $acc['bid'], 'bank' => $bank]);
//check accounting docs - include both approved and preview documents for deletion check
$rows = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.bank = :bank')
->setParameter('bid', $acc['bid'])
->setParameter('bank', $bank)
->getQuery()
->getResult();
if (count($rows) > 0)
return $this->json(['result' => 2]);
if ($acc['bid']->getWalletMatchBank()) {
@ -245,13 +293,21 @@ class BankController extends AbstractController
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bank' => $bank
]);
// Use query builder to filter by approved documents
$items = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bank = :bank')
->andWhere('d.isApproved = :isApproved')
->setParameter('bank', $bank)
->setParameter('isApproved', true)
->getQuery()
->getResult();
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
$bs += (float) $item->getBs();
$bd += (float) $item->getBd();
}
return $this->json([
@ -285,11 +341,20 @@ class BankController extends AbstractController
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bank = :bank')
->andWhere('r.bid = :bid')
->setParameter('bank', $bank)
->setParameter('bid', $acc['bid']);
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
if (isset($params['startDate']) && isset($params['endDate'])) {
$query->andWhere('r.doc.date BETWEEN :startDate AND :endDate')
->setParameter('startDate', $params['startDate'])
@ -327,12 +392,29 @@ class BankController extends AbstractController
$bank = $entityManager->getRepository(BankAccount::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$bank)
throw $this->createNotFoundException();
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'bank' => $bank,
'year'=>$acc['year']
]);
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.bank = :bank')
->andWhere('r.year = :year')
->setParameter('bid', $acc['bid'])
->setParameter('bank', $bank)
->setParameter('year', $acc['year']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$transactions = $query->getQuery()->getResult();
} else {
$transactions = [];
foreach ($params['items'] as $param) {
@ -343,7 +425,10 @@ class BankController extends AbstractController
'year' => $acc['year']
]);
if ($prs) {
$transactions[] = $prs;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $prs->getDoc()->isApproved()) {
$transactions[] = $prs;
}
}
}
}
@ -397,12 +482,28 @@ class BankController extends AbstractController
if (!$bank)
throw $this->createNotFoundException();
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'bank' => $bank,
'year'=>$acc['year']
]);
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.bank = :bank')
->andWhere('r.year = :year')
->setParameter('bid', $acc['bid'])
->setParameter('bank', $bank)
->setParameter('year', $acc['year']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$transactions = $query->getQuery()->getResult();
} else {
$transactions = [];
foreach ($params['items'] as $param) {
@ -413,7 +514,10 @@ class BankController extends AbstractController
'year'=>$acc['year']
]);
if ($prs) {
$transactions[] = $prs;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $prs->getDoc()->isApproved()) {
$transactions[] = $prs;
}
}
}
}

View file

@ -103,7 +103,21 @@ class BusinessController extends AbstractController
]);
if (!$perms)
throw $this->createAccessDeniedException();
return $this->json(Explore::ExploreBusiness($bus));
$result = Explore::ExploreBusiness($bus);
// Read approval settings from Business entity (only new fields)
$result['approvers'] = [
'sellInvoice' => $bus->getApproverSellInvoice(),
'buyInvoice' => $bus->getApproverBuyInvoice(),
'returnBuy' => $bus->getApproverReturnBuy(),
'returnSell' => $bus->getApproverReturnSell(),
'warehouseTransfer' => $bus->getApproverWarehouseTransfer(),
'receiveFromPersons' => $bus->getApproverReceiveFromPersons(),
'payToPersons' => $bus->getApproverPayToPersons(),
'accountingDocs' => $bus->getApproverAccountingDocs(),
'bankTransfers' => $bus->getApproverBankTransfers(),
];
return $this->json($result);
}
#[Route('/api/business/list/count', name: 'api_bussiness_list_count')]
@ -246,22 +260,26 @@ class BusinessController extends AbstractController
$business->setWalletEnable(false);
}
}
if (array_key_exists('requireTwoStepApproval', $params)) {
$business->setRequireTwoStepApproval((bool)$params['requireTwoStepApproval']);
}
// Set approvers
if (array_key_exists('invoiceApprover', $params)) {
$business->setInvoiceApprover($params['invoiceApprover']);
}
// Approval settings
$business->setRequireTwoStepApproval((bool)$params['requireTwoStepApproval'] ?? false);
if (array_key_exists('warehouseApprover', $params)) {
$business->setWarehouseApprover($params['warehouseApprover']);
}
$approvers = $params['approvers'] ?? [];
if (array_key_exists('financialApprover', $params)) {
$business->setFinancialApprover($params['financialApprover']);
}
$business->setApproverSellInvoice($approvers['sellInvoice'] ?? null);
$business->setApproverBuyInvoice($approvers['buyInvoice'] ?? null);
$business->setApproverReturnBuy($approvers['returnBuy'] ?? null);
$business->setApproverReturnSell($approvers['returnSell'] ?? null);
$business->setApproverWarehouseTransfer($approvers['warehouseTransfer'] ?? null);
$business->setApproverReceiveFromPersons($approvers['receiveFromPersons'] ?? null);
$business->setApproverPayToPersons($approvers['payToPersons'] ?? null);
$business->setApproverAccountingDocs($approvers['accountingDocs'] ?? null);
$business->setApproverBankTransfers($approvers['bankTransfers'] ?? null);
// Warranty settings
$business->setRequireWarrantyOnDelivery($params['requireWarrantyOnDelivery'] ?? false);
$business->setActivationGraceDays($params['activationGraceDays'] ?? 7);
$business->setMatchWarrantyToSerial($params['matchWarrantyToSerial'] ?? false);
//get Money type
if (!array_key_exists('arzmain', $params) && $isNew) {
@ -277,9 +295,12 @@ class BusinessController extends AbstractController
$business->setDateSubmit(time());
$entityManager->persist($business);
$entityManager->flush();
// No registry usage; settings persisted on Business entity
if ($isNew) {
$perms = new Permission();
$giftCredit = (int) $registryMGR->get('system_settings', 'gift_credit', 0);
$giftCreditRaw = $registryMGR->get('system_settings', 'gift_credit');
$giftCredit = (int) ($giftCreditRaw ?? 0);
$business->setSmsCharge($giftCredit);
$perms->setBid($business);
$perms->setUser($this->getUser());
@ -562,10 +583,13 @@ class BusinessController extends AbstractController
'plugGhestaManager' => true,
'plugTaxSettings' => true,
'plugWarranty' => true,
'plugImportWorkflow' => true,
'inquiry' => true,
'ai' => true,
'warehouseManager' => true,
'importWorkflow' => true,
'plugHrmAttendance' => true,
'storehelper' => true,
];
} elseif ($perm) {
$result = [
@ -611,18 +635,14 @@ class BusinessController extends AbstractController
'plugGhestaManager' => $perm->isPlugGhestaManager(),
'plugTaxSettings' => $perm->isPlugTaxSettings(),
'plugWarranty' => $perm->isPlugWarrantyManager(),
'plugImportWorkflow' => $perm->isImportWorkflow(),
'inquiry' => $perm->isInquiry(),
'ai' => $perm->isAi(),
'warehouseManager' => $perm->isWarehouseManager(),
'importWorkflow' => $perm->isImportWorkflow(),
'plugHrmAttendance' => $perm->isPlugHrmAttendance(),
'storehelper' => $perm->isStorehelper()
];
if ($perm->isWarehouseManager()) {
$result['commodity'] = true;
$result['store'] = true;
$result['plugWarranty'] = true;
$result['permission'] = true;
}
}
return $this->json($result);
}
@ -693,10 +713,13 @@ class BusinessController extends AbstractController
$perm->setPlugGhestaManager($params['plugGhestaManager']);
$perm->setPlugWarrantyManager($params['plugWarranty'] ?? false);
$perm->setPlugTaxSettings($params['plugTaxSettings']);
$perm->setImportWorkflow($params['plugImportWorkflow'] ?? false);
$perm->setInquiry($params['inquiry']);
$perm->setAi($params['ai']);
$perm->setWarehouseManager($params['warehouseManager'] ?? false);
$perm->setImportWorkflow($params['importWorkflow'] ?? false);
$perm->setPlugHrmAttendance($params['plugHrmAttendance'] ?? false);
$perm->setStorehelper($params['storehelper'] ?? false);
$entityManager->persist($perm);
$entityManager->flush();
$log->insert('تنظیمات پایه', 'ویرایش دسترسی‌های کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business);
@ -736,7 +759,8 @@ class BusinessController extends AbstractController
$docs = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $buss,
'year' => $year,
'money' => $acc['money']
'money' => $acc['money'],
'isApproved' => true
]);
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy([
@ -750,8 +774,8 @@ class BusinessController extends AbstractController
'bid' => $buss,
'year' => $year,
'type' => 'buy',
'money' => $acc['money']
'money' => $acc['money'],
'isApproved' => true
]);
$buysTotal = 0;
$buysToday = 0;
@ -773,7 +797,8 @@ class BusinessController extends AbstractController
'bid' => $buss,
'year' => $year,
'type' => 'sell',
'money' => $acc['money']
'money' => $acc['money'],
'isApproved' => true
]);
$sellsTotal = 0;
$sellsToday = 0;
@ -795,7 +820,8 @@ class BusinessController extends AbstractController
'bid' => $buss,
'year' => $year,
'type' => 'person_send',
'money' => $acc['money']
'money' => $acc['money'],
'isApproved' => true
]);
$sendsTotal = 0;
$sendsToday = 0;
@ -817,7 +843,8 @@ class BusinessController extends AbstractController
'bid' => $buss,
'year' => $year,
'type' => 'person_receive',
'money' => $acc['money']
'money' => $acc['money'],
'isApproved' => true
]);
$recsTotal = 0;
$recsToday = 0;

View file

@ -252,6 +252,9 @@ class BuyController extends AbstractController
return $this->json($extractor->notFound());
}
foreach ($params['items'] as $item) {
if (!$item || !isset($item['code'])) {
continue;
}
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
@ -286,77 +289,177 @@ class BuyController extends AbstractController
return $this->json($extractor->operationSuccess());
}
#[Route('/api/buy/docs/search', name: 'app_buy_docs_search')]
#[Route('/api/buy/docs/search', name: 'app_buy_docs_search', methods: ['POST'])]
public function app_buy_docs_search(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('buy');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
$params = json_decode($request->getContent(), true) ?? [];
$searchTerm = $params['search'] ?? '';
$page = max(1, $params['page'] ?? 1);
$perPage = max(1, min(100, $params['perPage'] ?? 10));
$types = $params['types'] ?? [];
$sortBy = $params['sortBy'] ?? [];
$queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('d.isPreview, d.isApproved')
->addSelect('u.fullName as submitter')
->addSelect('approver.fullName as approvedByName, approver.id as approvedById, approver.email as approvedByEmail')
->addSelect('l.code as labelCode, l.label as labelLabel')
->from(HesabdariDoc::class, 'd')
->leftJoin('d.submitter', 'u')
->leftJoin('d.approvedBy', 'approver')
->leftJoin('d.InvoiceLabel', 'l')
->leftJoin('d.hesabdariRows', 'r')
->where('d.bid = :bid')
->andWhere('d.year = :year')
->andWhere('d.type = :type')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('type', 'buy')
->setParameter('money', $acc['money']);
if ($searchTerm) {
$queryBuilder->leftJoin('r.person', 'p')
->andWhere(
$queryBuilder->expr()->orX(
'd.code LIKE :search',
'd.des LIKE :search',
'd.date LIKE :search',
'd.amount LIKE :search',
'p.nikename LIKE :search',
'p.mobile LIKE :search'
)
)
->setParameter('search', "%$searchTerm%");
}
$data = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'type' => 'buy',
'money' => $acc['money']
], [
'id' => 'DESC'
]);
if (!empty($types)) {
$queryBuilder->andWhere('l.code IN (:types)')
->setParameter('types', $types);
}
// فیلدهای معتبر برای مرتب‌سازی توی دیتابیس
$validDbFields = [
'id' => 'd.id',
'dateSubmit' => 'd.dateSubmit',
'date' => 'd.date',
'type' => 'd.type',
'code' => 'd.code',
'des' => 'd.des',
'amount' => 'd.amount',
'mdate' => 'd.mdate',
'plugin' => 'd.plugin',
'refData' => 'd.refData',
'shortlink' => 'd.shortlink',
'isPreview' => 'd.isPreview',
'isApproved' => 'd.isApproved',
'approvedBy' => 'd.approvedBy',
'submitter' => 'u.fullName',
'label' => 'l.label',
];
// اعمال مرتب‌سازی توی دیتابیس
if (!empty($sortBy)) {
foreach ($sortBy as $sort) {
$key = $sort['key'] ?? 'id';
$direction = isset($sort['order']) && strtoupper($sort['order']) === 'DESC' ? 'DESC' : 'ASC';
if (isset($validDbFields[$key])) {
$queryBuilder->addOrderBy($validDbFields[$key], $direction);
}
}
} else {
$queryBuilder->orderBy('d.id', 'DESC');
}
$totalItemsQuery = clone $queryBuilder;
$totalItems = $totalItemsQuery->select('COUNT(DISTINCT d.id)')
->getQuery()
->getSingleScalarResult();
$queryBuilder->setFirstResult(($page - 1) * $perPage)
->setMaxResults($perPage);
$docs = $queryBuilder->getQuery()->getArrayResult();
$dataTemp = [];
foreach ($data as $item) {
$temp = [
'id' => $item->getId(),
'dateSubmit' => $item->getDateSubmit(),
'date' => $item->getDate(),
'type' => $item->getType(),
'code' => $item->getCode(),
'des' => $item->getDes(),
'amount' => $item->getAmount(),
'submitter' => $item->getSubmitter()->getFullName(),
foreach ($docs as $doc) {
$item = [
'id' => $doc['id'],
'dateSubmit' => $doc['dateSubmit'],
'date' => $doc['date'],
'type' => $doc['type'],
'code' => $doc['code'],
'des' => $doc['des'],
'amount' => $doc['amount'],
'submitter' => $doc['submitter'],
'label' => $doc['labelCode'] ? [
'code' => $doc['labelCode'],
'label' => $doc['labelLabel']
] : null,
'isPreview' => $doc['isPreview'],
'isApproved' => $doc['isApproved'],
'approvedBy' => $doc['approvedByName'] ? [
'fullName' => $doc['approvedByName'],
'id' => $doc['approvedById'],
'email' => $doc['approvedByEmail']
] : null,
];
$mainRow = $entityManager->getRepository(HesabdariRow::class)->getNotEqual($item, 'person');
$temp['person'] = '';
if ($mainRow)
$temp['person'] = Explore::ExplorePerson($mainRow->getPerson());
$temp['label'] = null;
if ($item->getInvoiceLabel()) {
$temp['label'] = [
'code' => $item->getInvoiceLabel()->getCode(),
'label' => $item->getInvoiceLabel()->getLabel()
];
}
$mainRow = $entityManager->getRepository(HesabdariRow::class)
->createQueryBuilder('r')
->where('r.doc = :docId')
->andWhere('r.person IS NOT NULL')
->setParameter('docId', $doc['id'])
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
$item['person'] = $mainRow && $mainRow->getPerson() ? Explore::ExplorePerson($mainRow->getPerson()) : null;
$temp['relatedDocsCount'] = count($item->getRelatedDocs());
$pays = 0;
foreach ($item->getRelatedDocs() as $relatedDoc) {
$pays += $relatedDoc->getAmount();
}
$temp['relatedDocsPays'] = $pays;
// محاسبه پرداختی‌ها
$relatedDocs = $entityManager->getRepository(HesabdariDoc::class)
->createQueryBuilder('rd')
->select('SUM(rd.amount) as total_pays, COUNT(rd.id) as count_docs')
->innerJoin('rd.relatedDocs', 'rel')
->where('rel.id = :sourceDocId')
->andWhere('rd.bid = :bidId')
->setParameter('sourceDocId', $doc['id'])
->setParameter('bidId', $acc['bid']->getId())
->getQuery()
->getSingleResult();
$temp['commodities'] = [];
foreach ($item->getHesabdariRows() as $item) {
if ($item->getRef()->getCode() == '51') {
$temp['discountAll'] = $item->getBs();
} elseif ($item->getRef()->getCode() == '90') {
$temp['transferCost'] = $item->getBd();
}
if ($item->getCommodity()) {
$temp['commodities'][] = Explore::ExploreCommodity($item->getCommodity(), $item->getCommdityCount());
$item['relatedDocsCount'] = (int) $relatedDocs['count_docs'];
$item['relatedDocsPays'] = $relatedDocs['total_pays'] ?? 0;
// محاسبه کالاها و تخفیف/هزینه حمل
$item['commodities'] = [];
$item['discountAll'] = 0;
$item['transferCost'] = 0;
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy(['doc' => $doc['id']]);
foreach ($rows as $row) {
if ($row->getRef()->getCode() == '51') {
$item['discountAll'] = $row->getBs();
} elseif ($row->getRef()->getCode() == '90') {
$item['transferCost'] = $row->getBd();
} elseif ($row->getCommodity()) {
$item['commodities'][] = Explore::ExploreCommodity($row->getCommodity(), $row->getCommdityCount());
}
}
if (!array_key_exists('discountAll', $temp))
$temp['discountAll'] = 0;
if (!array_key_exists('transferCost', $temp))
$temp['transferCost'] = 0;
$dataTemp[] = $temp;
$dataTemp[] = $item;
}
return $this->json($dataTemp);
return $this->json([
'items' => $dataTemp,
'total' => (int) $totalItems,
'page' => $page,
'perPage' => $perPage,
]);
}
#[Route('/api/buy/posprinter/invoice', name: 'app_buy_posprinter_invoice')]
@ -425,6 +528,8 @@ class BuyController extends AbstractController
return $this->json(['id' => $pdfPid]);
}
#[Route('/api/buy/print/invoice', name: 'app_buy_print_invoice')]
public function app_buy_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, TemplateRenderer $renderer): JsonResponse
{
@ -586,4 +691,43 @@ class BuyController extends AbstractController
}
return $this->json(['id' => $pdfPid]);
}
#[Route('/api/buy/approve/{code}', name: 'app_buy_approve', methods: ['POST'])]
public function approveBuyDoc(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('buy');
if (!$acc) throw $this->createAccessDeniedException();
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
'money' => $acc['money']
]);
if (!$doc) throw $this->createNotFoundException('فاکتور یافت نشد');
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
$entityManager->persist($doc);
$entityManager->flush();
return $this->json(['result' => 0]);
}
#[Route('/api/buy/payment/approve/{code}', name: 'app_buy_payment_approve', methods: ['POST'])]
public function approveBuyPayment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('buy');
if (!$acc) throw $this->createAccessDeniedException();
$paymentDoc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
'money' => $acc['money'],
'type' => 'buy_pay'
]);
if (!$paymentDoc) throw $this->createNotFoundException('سند پرداخت یافت نشد');
$paymentDoc->setIsPreview(false);
$paymentDoc->setIsApproved(true);
$paymentDoc->setApprovedBy($this->getUser());
$entityManager->persist($paymentDoc);
$entityManager->flush();
return $this->json(['result' => 0]);
}
}

View file

@ -49,9 +49,17 @@ class CashdeskController extends AbstractController
foreach ($datas as $data) {
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'cashdesk' => $data
]);
// Use query builder to filter by approved documents
$items = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.cashdesk = :cashdesk')
->andWhere('d.isApproved = :isApproved')
->setParameter('cashdesk', $data)
->setParameter('isApproved', true)
->getQuery()
->getResult();
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
@ -131,8 +139,17 @@ class CashdeskController extends AbstractController
$cashdesk = $entityManager->getRepository(Cashdesk::class)->findOneBy(['bid' => $acc['bid'], 'code' => $code]);
if (!$cashdesk)
throw $this->createNotFoundException();
//check accounting docs
$rows = $entityManager->getRepository(HesabdariRow::class)->findby(['bid' => $acc['bid'], 'cashdesk' => $cashdesk]);
//check accounting docs - include both approved and preview documents for deletion check
$rows = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.cashdesk = :cashdesk')
->setParameter('bid', $acc['bid'])
->setParameter('cashdesk', $cashdesk)
->getQuery()
->getResult();
if (count($rows) > 0)
return $this->json(['result' => 2]);
@ -177,9 +194,17 @@ class CashdeskController extends AbstractController
foreach ($datas as $data) {
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'cashdesk' => $data
]);
// Use query builder to filter by approved documents
$items = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.cashdesk = :cashdesk')
->andWhere('d.isApproved = :isApproved')
->setParameter('cashdesk', $data)
->setParameter('isApproved', true)
->getQuery()
->getResult();
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
@ -211,9 +236,23 @@ class CashdeskController extends AbstractController
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'cashdesk' => $cashdesk
]);
// Use query builder to filter by approved documents
$items = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.cashdesk = :cashdesk')
->andWhere('r.year = :year')
->andWhere('r.bid = :bid')
->andWhere('r.money = :money')
->andWhere('d.isApproved = :isApproved')
->setParameter('cashdesk', $cashdesk)
->setParameter('year', $acc['year'])
->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money'])
->setParameter('isApproved', true)
->getQuery()
->getResult();
foreach ($items as $item) {
$bs += $item->getBs();
@ -251,11 +290,20 @@ class CashdeskController extends AbstractController
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.cashdesk = :cashdesk')
->andWhere('r.bid = :bid')
->setParameter('cashdesk', $cashdesk)
->setParameter('bid', $acc['bid']);
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
if (isset($params['startDate']) && isset($params['endDate'])) {
$query->andWhere('r.doc.date BETWEEN :startDate AND :endDate')
->setParameter('startDate', $params['startDate'])
@ -293,12 +341,29 @@ class CashdeskController extends AbstractController
$cashdesk = $entityManager->getRepository(Cashdesk::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$cashdesk)
throw $this->createNotFoundException();
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'cashdesk' => $cashdesk,
'year'=>$acc['year']
]);
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.cashdesk = :cashdesk')
->andWhere('r.year = :year')
->setParameter('bid', $acc['bid'])
->setParameter('cashdesk', $cashdesk)
->setParameter('year', $acc['year']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$transactions = $query->getQuery()->getResult();
} else {
$transactions = [];
foreach ($params['items'] as $param) {
@ -309,7 +374,10 @@ class CashdeskController extends AbstractController
'year' => $acc['year']
]);
if ($prs) {
$transactions[] = $prs;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $prs->getDoc()->isApproved()) {
$transactions[] = $prs;
}
}
}
}
@ -370,12 +438,28 @@ class CashdeskController extends AbstractController
if (!$cashdesk)
throw $this->createNotFoundException();
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'cashdesk' => $cashdesk,
'year'=>$acc['year']
]);
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.cashdesk = :cashdesk')
->andWhere('r.year = :year')
->setParameter('bid', $acc['bid'])
->setParameter('cashdesk', $cashdesk)
->setParameter('year', $acc['year']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$transactions = $query->getQuery()->getResult();
} else {
$transactions = [];
foreach ($params['items'] as $param) {
@ -386,7 +470,10 @@ class CashdeskController extends AbstractController
'year'=>$acc['year']
]);
if ($prs) {
$transactions[] = $prs;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $prs->getDoc()->isApproved()) {
$transactions[] = $prs;
}
}
}
}

View file

@ -892,4 +892,135 @@ class ChequeController extends AbstractController
'result' => 'ok'
]);
}
#[Route('/api/cheque/dashboard/stats', name: 'app_cheque_dashboard_stats')]
public function app_cheque_dashboard_stats(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, Jdate $jdate): JsonResponse
{
$acc = $access->hasRole('cheque');
if (!$acc)
throw $this->createAccessDeniedException();
$money = $acc['money'];
$defaultMoey = $acc['bid']->getMoney();
$defMoney = false;
if ($defaultMoey->getId() == $money->getId()) {
$defMoney = true;
}
// آمار کلی چک‌ها
$qb = $entityManager->createQueryBuilder();
$totalInputCheques = $qb->select('COUNT(c.id)')
->from(Cheque::class, 'c')
->where('c.bid = :bid')
->andWhere('c.type = :type')
->andWhere($defMoney ? '(c.money = :money OR c.money IS NULL)' : 'c.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('type', 'input')
->setParameter('money', $money)
->getQuery()
->getSingleScalarResult();
$qb = $entityManager->createQueryBuilder();
$totalOutputCheques = $qb->select('COUNT(c.id)')
->from(Cheque::class, 'c')
->where('c.bid = :bid')
->andWhere('c.type = :type')
->andWhere($defMoney ? '(c.money = :money OR c.money IS NULL)' : 'c.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('type', 'output')
->setParameter('money', $money)
->getQuery()
->getSingleScalarResult();
// چک‌های سررسید امروز
$today = $jdate->jdate('Y/m/d', time());
$qb = $entityManager->createQueryBuilder();
$todayDueCheques = $qb->select('c')
->from(Cheque::class, 'c')
->where('c.bid = :bid')
->andWhere($defMoney ? '(c.money = :money OR c.money IS NULL)' : 'c.money = :money')
->andWhere('c.date = :today')
->andWhere('c.status != :rejected')
->setParameter('bid', $acc['bid'])
->setParameter('money', $money)
->setParameter('today', $today)
->setParameter('rejected', 'برگشت خورده')
->getQuery()
->getResult();
// چک‌های نزدیک به سررسید (7 روز آینده)
$endDate = $jdate->jdate('Y/m/d', strtotime('+7 days'));
$qb = $entityManager->createQueryBuilder();
$soonDueCheques = $qb->select('c')
->from(Cheque::class, 'c')
->where('c.bid = :bid')
->andWhere($defMoney ? '(c.money = :money OR c.money IS NULL)' : 'c.money = :money')
->andWhere('c.date >= :start')
->andWhere('c.date <= :end')
->andWhere('c.status != :rejected')
->setParameter('bid', $acc['bid'])
->setParameter('money', $money)
->setParameter('start', $today)
->setParameter('end', $endDate)
->setParameter('rejected', 'برگشت خورده')
->orderBy('c.date', 'ASC')
->getQuery()
->getResult();
// آمار وضعیت چک‌ها
$qb = $entityManager->createQueryBuilder();
$statusStats = $qb->select('c.status, COUNT(c.id) as count, SUM(c.amount) as total_amount')
->from(Cheque::class, 'c')
->where('c.bid = :bid')
->andWhere($defMoney ? '(c.money = :money OR c.money IS NULL)' : 'c.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('money', $money)
->groupBy('c.status')
->getQuery()
->getResult();
// آمار ماهانه چک‌ها (6 ماه اخیر) - ساده‌سازی شده
$sixMonthsAgo = $jdate->jdate('Y/m', strtotime('-6 months')) . '/01';
$qb = $entityManager->createQueryBuilder();
$allCheques = $qb->select('c.date, c.type, c.amount')
->from(Cheque::class, 'c')
->where('c.bid = :bid')
->andWhere($defMoney ? '(c.money = :money OR c.money IS NULL)' : 'c.money = :money')
->andWhere('c.date >= :sixMonthsAgo')
->setParameter('bid', $acc['bid'])
->setParameter('money', $money)
->setParameter('sixMonthsAgo', $sixMonthsAgo)
->getQuery()
->getResult();
// پردازش داده‌های ماهانه
$processedMonthlyStats = [];
foreach ($allCheques as $cheque) {
$month = substr($cheque['date'], 0, 7); // YYYY/MM
$key = $month . '_' . $cheque['type'];
if (!isset($processedMonthlyStats[$key])) {
$processedMonthlyStats[$key] = [
'month' => $month,
'type' => $cheque['type'],
'count' => 0,
'total_amount' => 0
];
}
$processedMonthlyStats[$key]['count']++;
$processedMonthlyStats[$key]['total_amount'] += (float)($cheque['amount'] ?? 0);
}
$monthlyStats = array_values($processedMonthlyStats);
return $this->json([
'totalInputCheques' => $totalInputCheques,
'totalOutputCheques' => $totalOutputCheques,
'todayDueCheques' => Explore::SerializeCheques($todayDueCheques),
'soonDueCheques' => Explore::SerializeCheques($soonDueCheques),
'statusStats' => $statusStats,
'monthlyStats' => $monthlyStats
]);
}
}

View file

@ -0,0 +1,433 @@
<?php
namespace App\Controller;
use App\Entity\BankAccount;
use App\Entity\Business;
use App\Entity\Cashdesk;
use App\Entity\HesabdariDoc;
use App\Entity\HesabdariRow;
use App\Entity\HesabdariTable;
use App\Entity\Log as EntityLog;
use App\Entity\Money;
use App\Entity\Person;
use App\Entity\Salary;
use App\Entity\Year;
use App\Service\Access;
use App\Service\CloseYearService;
use App\Service\Explore;
use App\Service\Jdate;
use App\Service\Log;
use App\Service\Provider;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class CloseYearController extends AbstractController
{
private CloseYearService $closeYearService;
public function __construct(CloseYearService $closeYearService)
{
$this->closeYearService = $closeYearService;
}
#[Route('/api/year/close/prepare', name: 'app_year_close_prepare')]
public function app_year_close_prepare(Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('plugAccproCloseYear');
if (!$acc)
throw $this->createAccessDeniedException();
$currentYear = $entityManager->getRepository(Year::class)->findOneBy([
'bid' => $acc['bid'],
'head' => true
]);
if (!$currentYear) {
return $this->json([
'result' => 0,
'msg' => 'سال مالی فعال یافت نشد'
]);
}
// محاسبه سود و زیان
$profitLoss = $this->closeYearService->calculateProfitLoss($currentYear);
// محاسبه ترازنامه
$balanceSheet = $this->closeYearService->calculateBalanceSheet($currentYear);
// دریافت ساختار درختی حساب‌ها
$accountsTree = $this->getAccountsTree($entityManager, $acc['bid'], $currentYear);
return $this->json([
'result' => 1,
'currentYear' => [
'id' => $currentYear->getId(),
'label' => $currentYear->getLabel(),
'start' => $currentYear->getStart(),
'end' => $currentYear->getEnd()
],
'profitLoss' => $profitLoss,
'balanceSheet' => $balanceSheet,
'accountsTree' => $accountsTree
]);
}
#[Route('/api/year/close/execute', name: 'app_year_close_execute', methods: ['POST'])]
public function app_year_close_execute(Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, Jdate $jdate, Provider $provider): JsonResponse
{
$acc = $access->hasRole('plugAccproCloseYear');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$currentYear = $entityManager->getRepository(Year::class)->findOneBy([
'bid' => $acc['bid'],
'head' => true
]);
if (!$currentYear) {
return $this->json([
'result' => 0,
'msg' => 'سال مالی فعال یافت نشد'
]);
}
try {
$entityManager->beginTransaction();
// بستن حساب‌های موقت
$this->closeYearService->closeTemporaryAccounts($currentYear, $params, $this->getUser());
// ایجاد سال مالی جدید
$newYear = $this->closeYearService->createNewYear($acc['bid'], $params);
// انتقال مانده حساب‌های دائمی
$this->closeYearService->transferPermanentAccounts($currentYear, $newYear, $this->getUser());
// ثبت سود انباشته
$this->closeYearService->recordRetainedEarnings($currentYear, $newYear, $params, $this->getUser());
// تغییر وضعیت سال مالی
$currentYear->setHead(false);
$newYear->setHead(true);
$entityManager->persist($currentYear);
$entityManager->persist($newYear);
$entityManager->flush();
$entityManager->commit();
$log->insert(
'بستن سال مالی',
'سال مالی ' . $currentYear->getLabel() . ' بسته شد و سال مالی جدید ' . $newYear->getLabel() . ' ایجاد شد.',
$this->getUser(),
$request->headers->get('activeBid'),
null
);
return $this->json([
'result' => 1,
'msg' => 'سال مالی با موفقیت بسته شد',
'newYear' => [
'id' => $newYear->getId(),
'label' => $newYear->getLabel()
]
]);
} catch (\Exception $e) {
$entityManager->rollback();
return $this->json([
'result' => 0,
'msg' => 'خطا در بستن سال مالی: ' . $e->getMessage()
]);
}
}
#[Route('/api/year/close/validate', name: 'app_year_close_validate', methods: ['POST'])]
public function app_year_close_validate(Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('plugAccproCloseYear');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
try {
// اعتبارسنجی تاریخ‌های سال مالی جدید
$this->closeYearService->validateNewYearDates($acc['bid'], $params);
return $this->json([
'result' => 1,
'msg' => 'تاریخ‌های سال مالی معتبر است',
'error' => false
]);
} catch (\InvalidArgumentException $e) {
return $this->json([
'result' => 0,
'msg' => $e->getMessage(),
'error' => true
]);
}
}
#[Route('/api/year/close/accounts', name: 'app_year_close_accounts')]
public function app_year_close_accounts(Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('plugAccproCloseYear');
if (!$acc)
throw $this->createAccessDeniedException();
$currentYear = $entityManager->getRepository(Year::class)->findOneBy([
'bid' => $acc['bid'],
'head' => true
]);
if (!$currentYear) {
return $this->json([
'result' => 0,
'msg' => 'سال مالی فعال یافت نشد'
]);
}
// دریافت تمام حساب‌های حسابداری به صورت درختی
$accounts = $this->getAccountsTree($entityManager, $acc['bid'], $currentYear);
return $this->json([
'result' => 1,
'accounts' => $accounts
]);
}
/**
* دریافت ساختار درختی حساب‌های حسابداری
*/
private function getAccountsTree(EntityManagerInterface $entityManager, Business $business, Year $year): array
{
$accountsTree = [];
// دریافت حساب‌های عمومی اصلی (بدون parent)
$publicMainAccounts = $entityManager->getRepository(HesabdariTable::class)->findBy([
'bid' => null,
'upper' => null
]);
foreach ($publicMainAccounts as $mainAccount) {
$accountData = $this->buildAccountTreeData($entityManager, $mainAccount, $year, $business);
if ($accountData) {
$accountData['isPublic'] = true;
$accountsTree[] = $accountData;
}
}
// دریافت حساب‌های اختصاصی اصلی (بدون parent)
$privateMainAccounts = $entityManager->getRepository(HesabdariTable::class)->findBy([
'bid' => $business,
'upper' => null
]);
foreach ($privateMainAccounts as $mainAccount) {
$accountData = $this->buildAccountTreeData($entityManager, $mainAccount, $year, $business);
if ($accountData) {
$accountData['isPublic'] = false;
$accountsTree[] = $accountData;
}
}
return $accountsTree;
}
/**
* ساخت داده‌های درختی حساب
*/
private function buildAccountTreeData(EntityManagerInterface $entityManager, HesabdariTable $account, Year $year, Business $business): ?array
{
// محاسبه مانده کل حساب و زیرمجموعه‌های آن
$totalBalance = $this->calculateAccountTreeBalance($entityManager, $account, $year, $business);
// محاسبه مانده با در نظر گرفتن نوع حساب
$balanceWithType = $this->calculateBalanceWithType($account->getCode(), $totalBalance);
// اگر مانده صفر است و زیرمجموعه‌ای ندارد، نمایش نده
if ($balanceWithType == 0 && !$this->hasChildren($entityManager, $account, $business)) {
return null;
}
$accountData = [
'id' => $account->getId(),
'name' => $account->getName(),
'code' => $account->getCode(),
'type' => $account->getType(),
'balance' => $balanceWithType,
'children' => []
];
// دریافت زیرمجموعه‌ها (عمومی و اختصاصی)
$childAccounts = $this->getChildAccounts($entityManager, $account, $business);
foreach ($childAccounts as $childAccount) {
$childData = $this->buildAccountTreeData($entityManager, $childAccount, $year, $business);
if ($childData) {
$childData['isPublic'] = $childAccount->getBid() === null;
$accountData['children'][] = $childData;
}
}
return $accountData;
}
/**
* محاسبه مانده کل یک حساب و تمام زیرمجموعه‌های آن
*/
private function calculateAccountTreeBalance(EntityManagerInterface $entityManager, HesabdariTable $account, Year $year, Business $business): float
{
$totalBalance = 0;
// محاسبه مانده خود حساب
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy([
'ref' => $account,
'year' => $year
]);
foreach ($rows as $row) {
// محاسبه مانده بر اساس کد حساب به جای type
$totalBalance += (float)$row->getBd() - (float)$row->getBs();
}
// محاسبه مانده تمام زیرمجموعه‌ها (عمومی و اختصاصی)
$childAccounts = $this->getChildAccounts($entityManager, $account, $business);
foreach ($childAccounts as $childAccount) {
$totalBalance += $this->calculateAccountTreeBalance($entityManager, $childAccount, $year, $business);
}
return $totalBalance;
}
/**
* محاسبه مانده با در نظر گرفتن نوع حساب
*/
private function calculateBalanceWithType(string $code, float $balance): float
{
if ($this->isDebitAccount($code)) {
return $balance; // بدهکار: بدهی - بستانکاری
} else {
return -$balance; // بستانکار: بستانکاری - بدهی
}
}
/**
* بررسی اینکه آیا حساب بدهکار است یا بستانکار
*/
private function isDebitAccount(string $code): bool
{
$codeInt = (int)$code;
// دارایی‌ها (کدهای 2-19، به جز 8 و 9): بدهکار
if ($codeInt >= 2 && $codeInt <= 19 && $codeInt != 8 && $codeInt != 9) {
return true;
}
// موجودی کالا (کد 120): بدهکار
if ($codeInt == 120) {
return true;
}
// حساب‌های کنترلی (کد 117): بدهکار
if ($codeInt == 117) {
return true;
}
// هزینه‌ها (کدهای 67-111): بدهکار
if ($codeInt >= 67 && $codeInt <= 111) {
return true;
}
// بدهی‌ها (کدهای 6-39): بستانکار
if ($codeInt >= 6 && $codeInt <= 39) {
return false;
}
// سرمایه (کدهای 40-47): بستانکار
if ($codeInt >= 40 && $codeInt <= 47) {
return false;
}
// درآمدها (کدهای 56-66): بستانکار
if ($codeInt >= 56 && $codeInt <= 66) {
return false;
}
// فروش (کدهای 52-55): بستانکار
if ($codeInt >= 52 && $codeInt <= 55) {
return false;
}
// بهای تمام شده (کدهای 48-51): بدهکار
if ($codeInt >= 48 && $codeInt <= 51) {
return true;
}
// سایر حساب‌ها: پیش‌فرض بستانکار
return false;
}
/**
* دریافت زیرمجموعه‌های یک حساب (عمومی و اختصاصی)
*/
private function getChildAccounts(EntityManagerInterface $entityManager, HesabdariTable $parentAccount, Business $business): array
{
$childAccounts = [];
// دریافت زیرمجموعه‌های عمومی
$publicChildren = $entityManager->getRepository(HesabdariTable::class)->findBy([
'upper' => $parentAccount,
'bid' => null
]);
foreach ($publicChildren as $child) {
$childAccounts[] = $child;
}
// دریافت زیرمجموعه‌های اختصاصی
$privateChildren = $entityManager->getRepository(HesabdariTable::class)->findBy([
'upper' => $parentAccount,
'bid' => $business
]);
foreach ($privateChildren as $child) {
$childAccounts[] = $child;
}
return $childAccounts;
}
/**
* بررسی وجود زیرمجموعه
*/
private function hasChildren(EntityManagerInterface $entityManager, HesabdariTable $account, Business $business): bool
{
// بررسی زیرمجموعه‌های عمومی
$publicChildCount = $entityManager->getRepository(HesabdariTable::class)->count([
'upper' => $account,
'bid' => null
]);
// بررسی زیرمجموعه‌های اختصاصی
$privateChildCount = $entityManager->getRepository(HesabdariTable::class)->count([
'upper' => $account,
'bid' => $business
]);
return ($publicChildCount + $privateChildCount) > 0;
}
}

View file

@ -133,10 +133,19 @@ class CommodityController extends AbstractController
$data = array_map(function (Commodity $item) use ($entityManager, $acc, $explore) {
$temp = $explore::ExploreCommodity($item);
if (!$item->isKhadamat()) {
$rows = $entityManager->getRepository('App\Entity\HesabdariRow')->findBy([
'bid' => $acc['bid'],
'commodity' => $item
]);
// Use query builder to filter by approved documents
$rows = $entityManager->createQueryBuilder()
->select('r')
->from('App\Entity\HesabdariRow', 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.commodity = :commodity')
->andWhere('d.isApproved = :isApproved')
->setParameter('bid', $acc['bid'])
->setParameter('commodity', $item)
->setParameter('isApproved', true)
->getQuery()
->getResult();
$count = 0;
foreach ($rows as $row) {
if ($row->getDoc()->getType() === 'buy' || $row->getDoc()->getType() === 'open_balance') {
@ -184,10 +193,19 @@ class CommodityController extends AbstractController
foreach ($items as $item) {
$temp = Explore::ExploreCommodity($item);
if (!$item->isKhadamat()) {
$rows = $entityManager->getRepository('App\Entity\HesabdariRow')->findBy([
'bid' => $acc['bid'],
'commodity' => $item
]);
// Use query builder to filter by approved documents
$rows = $entityManager->createQueryBuilder()
->select('r')
->from('App\Entity\HesabdariRow', 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.commodity = :commodity')
->andWhere('d.isApproved = :isApproved')
->setParameter('bid', $acc['bid'])
->setParameter('commodity', $item)
->setParameter('isApproved', true)
->getQuery()
->getResult();
$count = 0;
foreach ($rows as $row) {
if ($row->getDoc()->getType() === 'buy' || $row->getDoc()->getType() === 'open_balance') {
@ -288,10 +306,19 @@ class CommodityController extends AbstractController
if ($item->isKhadamat()) {
$temp['count'] = 0;
} else {
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'commodity' => $item
]);
// Use query builder to filter by approved documents
$rows = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.commodity = :commodity')
->andWhere('d.isApproved = :isApproved')
->setParameter('bid', $acc['bid'])
->setParameter('commodity', $item)
->setParameter('isApproved', true)
->getQuery()
->getResult();
$count = 0;
foreach ($rows as $row) {
if ($row->getDoc()->getType() == 'buy') {
@ -356,10 +383,19 @@ class CommodityController extends AbstractController
if ($item->isKhadamat()) {
$temp['count'] = 0;
} else {
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'commodity' => $item
]);
// Use query builder to filter by approved documents
$rows = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.commodity = :commodity')
->andWhere('d.isApproved = :isApproved')
->setParameter('bid', $acc['bid'])
->setParameter('commodity', $item)
->setParameter('isApproved', true)
->getQuery()
->getResult();
$count = 0;
foreach ($rows as $row) {
if ($row->getDoc()->getType() == 'buy') {
@ -452,10 +488,19 @@ class CommodityController extends AbstractController
if ($item->isKhadamat()) {
$temp['count'] = 0;
} else {
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'commodity' => $item
]);
// Use query builder to filter by approved documents
$rows = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.commodity = :commodity')
->andWhere('d.isApproved = :isApproved')
->setParameter('bid', $acc['bid'])
->setParameter('commodity', $item)
->setParameter('isApproved', true)
->getQuery()
->getResult();
$count = 0;
foreach ($rows as $row) {
if ($row->getDoc()->getType() == 'buy') {
@ -651,10 +696,19 @@ class CommodityController extends AbstractController
if ($data->isKhadamat()) {
$res['count'] = 0;
} else {
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'commodity' => $data
]);
// Use query builder to filter by approved documents
$rows = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.commodity = :commodity')
->andWhere('d.isApproved = :isApproved')
->setParameter('bid', $acc['bid'])
->setParameter('commodity', $data)
->setParameter('isApproved', true)
->getQuery()
->getResult();
foreach ($rows as $row) {
if ($row->getDoc()->getType() == 'buy') {
$count += $row->getCommdityCount();
@ -1220,8 +1274,8 @@ class CommodityController extends AbstractController
unset($data[0]);
foreach ($data as $item) {
//load cat
$unit = $entityManager->getRepository(commodity::class)->findOneBy([
//load unit
$unit = $entityManager->getRepository(CommodityUnit::class)->findOneBy([
'name' => $item[7],
]);
if (!$unit) {
@ -1320,8 +1374,17 @@ class CommodityController extends AbstractController
throw $this->createNotFoundException('کالا یافت نشد');
}
// بررسی اسناد حسابداری
$docs = $entityManager->getRepository(HesabdariRow::class)->findBy(['bid' => $acc['bid'], 'commodity' => $commodity]);
// بررسی اسناد حسابداری - include both approved and preview documents for deletion check
$docs = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.commodity = :commodity')
->setParameter('bid', $acc['bid'])
->setParameter('commodity', $commodity)
->getQuery()
->getResult();
if (count($docs) > 0) {
return $this->json(['result' => 2, 'message' => 'این کالا در اسناد حسابداری استفاده شده و قابل حذف نیست']);
}
@ -1371,7 +1434,17 @@ class CommodityController extends AbstractController
continue;
}
$docs = $entityManager->getRepository(HesabdariRow::class)->findBy(['bid' => $acc['bid'], 'commodity' => $commodity]);
// بررسی اسناد حسابداری - include both approved and preview documents for deletion check
$docs = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.commodity = :commodity')
->setParameter('bid', $acc['bid'])
->setParameter('commodity', $commodity)
->getQuery()
->getResult();
$storeDocs = $entityManager->getRepository(StoreroomItem::class)->findBy(['bid' => $acc['bid'], 'commodity' => $commodity]);
if (count($docs) > 0 || count($storeDocs) > 0) {

View file

@ -47,7 +47,7 @@ class CostController extends AbstractController
$yearStart = $jdate->jdate('Y/m/d', $yearStartUnix);
$yearEnd = $jdate->jdate('Y/m/d', $yearEndUnix);
// کوئری پایه - فقط جمع bd را محاسبه می‌کنیم
// کوئری پایه - فقط جمع bd را محاسبه می‌کنیم و فقط اسناد تایید شده
$qb = $entityManager->createQueryBuilder()
->select('SUM(COALESCE(r.bd, 0)) as total')
->from('App\Entity\HesabdariDoc', 'd')
@ -56,10 +56,12 @@ class CostController extends AbstractController
->andWhere('d.money = :money')
->andWhere('d.type = :type')
->andWhere('d.year = :year')
->andWhere('d.isApproved = :isApproved')
->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money'])
->setParameter('type', 'cost')
->setParameter('year', $acc['year']);
->setParameter('year', $acc['year'])
->setParameter('isApproved', true);
// هزینه امروز
$todayCost = (clone $qb)
@ -126,7 +128,7 @@ class CostController extends AbstractController
'year' => $acc['year'],
];
// کوئری پایه
// کوئری پایه - فقط اسناد تایید شده
$qb = $entityManager->createQueryBuilder()
->select('t.name AS center_name, SUM(COALESCE(r.bd, 0)) AS total_cost')
->from('App\Entity\HesabdariDoc', 'd')
@ -136,13 +138,15 @@ class CostController extends AbstractController
->andWhere('d.money = :money')
->andWhere('d.type = :type')
->andWhere('d.year = :year')
->andWhere('d.isApproved = :isApproved')
->andWhere('r.bd != 0')
->groupBy('t.id, t.name')
->orderBy('total_cost', 'DESC')
->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money'])
->setParameter('type', 'cost')
->setParameter('year', $acc['year']);
->setParameter('year', $acc['year'])
->setParameter('isApproved', true);
// اعمال فیلتر تاریخ فقط برای امروز و ماه
if ($period === 'today') {
@ -203,6 +207,7 @@ class CostController extends AbstractController
// Build base query
$queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('d.isPreview, d.isApproved')
->addSelect('u.fullName as submitter')
->from('App\Entity\HesabdariDoc', 'd')
->leftJoin('d.submitter', 'u')
@ -217,6 +222,14 @@ class CostController extends AbstractController
->setParameter('type', $type)
->setParameter('money', $acc['money']);
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!$includePreview) {
// Default: only show approved documents
$queryBuilder->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
// Apply filters
if (!empty($filters)) {
// Text search
@ -313,6 +326,8 @@ class CostController extends AbstractController
'des' => $doc['des'],
'amount' => $doc['amount'],
'submitter' => $doc['submitter'],
'isPreview' => $doc['isPreview'],
'isApproved' => $doc['isApproved'],
];
// Get cost center details
@ -378,14 +393,30 @@ class CostController extends AbstractController
$params = json_decode($request->getContent(), true) ?? [];
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
// دریافت آیتم‌های انتخاب شده یا همه آیتم‌ها
if (!isset($params['items'])) {
$items = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'type' => 'cost',
'year' => $acc['year'],
'money' => $acc['money']
]);
$query = $entityManager->createQueryBuilder()
->select('d')
->from(HesabdariDoc::class, 'd')
->where('d.bid = :bid')
->andWhere('d.type = :type')
->andWhere('d.year = :year')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('type', 'cost')
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$items = $query->getQuery()->getResult();
} else {
$items = [];
foreach ($params['items'] as $param) {
@ -397,7 +428,10 @@ class CostController extends AbstractController
'money' => $acc['money']
]);
if ($doc) {
$items[] = $doc;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $doc->isApproved()) {
$items[] = $doc;
}
}
}
}
@ -429,14 +463,30 @@ class CostController extends AbstractController
$params = json_decode($request->getContent(), true) ?? [];
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
// دریافت آیتم‌های انتخاب شده یا همه آیتم‌ها
if (!isset($params['items'])) {
$items = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'type' => 'cost',
'year' => $acc['year'],
'money' => $acc['money']
]);
$query = $entityManager->createQueryBuilder()
->select('d')
->from(HesabdariDoc::class, 'd')
->where('d.bid = :bid')
->andWhere('d.type = :type')
->andWhere('d.year = :year')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('type', 'cost')
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$items = $query->getQuery()->getResult();
} else {
$items = [];
foreach ($params['items'] as $param) {
@ -448,7 +498,10 @@ class CostController extends AbstractController
'money' => $acc['money']
]);
if ($doc) {
$items[] = $doc;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $doc->isApproved()) {
$items[] = $doc;
}
}
}
}
@ -566,6 +619,20 @@ class CostController extends AbstractController
$doc->setMoney($acc['money']);
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
// Set approval status based on business settings
$business = $acc['bid'];
if ($business->isRequireTwoStepApproval()) {
// Two-step approval is enabled
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
} else {
// Two-step approval is disabled - auto approve
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
}
$entityManager->persist($doc);
$entityManager->flush();

View file

@ -68,6 +68,11 @@ class DashboardController extends AbstractController
if(array_key_exists('topCostCenters',$params)) $setting->setTopCostCenters($params['topCostCenters']);
if(array_key_exists('incomes',$params)) $setting->setIncomes($params['incomes']);
if(array_key_exists('topIncomeCenters',$params)) $setting->setTopIncomesChart($params['topIncomeCenters']);
if(array_key_exists('cheques',$params)) $setting->setCheques($params['cheques']);
if(array_key_exists('chequesDueToday',$params)) $setting->setChequesDueToday($params['chequesDueToday']);
if(array_key_exists('chequesStatusChart',$params)) $setting->setChequesStatusChart($params['chequesStatusChart']);
if(array_key_exists('chequesMonthlyChart',$params)) $setting->setChequesMonthlyChart($params['chequesMonthlyChart']);
if(array_key_exists('chequesDueSoon',$params)) $setting->setChequesDueSoon($params['chequesDueSoon']);
$entityManagerInterface->persist($setting);
$entityManagerInterface->flush();

View file

@ -42,6 +42,20 @@ class DirectHesabdariDoc extends AbstractController
$hesabdariDoc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
$hesabdariDoc->setDateSubmit(time());
// Set approval status based on business settings
$business = $acc['bid'];
if ($business->isRequireTwoStepApproval()) {
// Two-step approval is enabled
$hesabdariDoc->setIsPreview(true);
$hesabdariDoc->setIsApproved(false);
$hesabdariDoc->setApprovedBy(null);
} else {
// Two-step approval is disabled - auto approve
$hesabdariDoc->setIsPreview(false);
$hesabdariDoc->setIsApproved(true);
$hesabdariDoc->setApprovedBy($this->getUser());
}
//insert rows
if (isset($prams['rows'])) {
if (count($prams['rows']) < 2) {
@ -355,6 +369,9 @@ class DirectHesabdariDoc extends AbstractController
'date' => $hesabdariDoc->getDate(),
'des' => $hesabdariDoc->getDes(),
'code' => $hesabdariDoc->getCode(),
'isPreview' => $hesabdariDoc->isPreview(),
'isApproved' => $hesabdariDoc->isApproved(),
'approvedBy' => $hesabdariDoc->getApprovedBy() ? $hesabdariDoc->getApprovedBy()->getFullName() : null,
'rows' => $rows
];

View file

@ -51,6 +51,7 @@ class ExploreAccountsController extends AbstractController
$page = $params['page'] ?? 1;
$perPage = $params['perPage'] ?? 10;
$offset = ($page - 1) * $perPage;
$dateFilter = $params['dateFilter'] ?? null;
$nodeId = $params['node'] === 'root'
? $this->em->getRepository(HesabdariTable::class)
@ -80,7 +81,7 @@ class ExploreAccountsController extends AbstractController
foreach ($children as $child) {
$allNodes = $this->getAllDescendants($child, $acc);
$allNodes[] = $child;
$rows = $this->getRowsForNodes($allNodes, $acc);
$rows = $this->getRowsForNodes($allNodes, $acc, $dateFilter);
$output[] = $this->calculateTotals($rows, $child, $acc);
}
break;
@ -101,7 +102,7 @@ class ExploreAccountsController extends AbstractController
'ref' => $node,
'bid' => $acc['bid'],
'year' => $acc['year'],
], $acc['money']);
], $acc['money'], $dateFilter);
$output[] = $this->calculateBankTotals($rows, $bankAccount, $node);
}
break;
@ -122,7 +123,7 @@ class ExploreAccountsController extends AbstractController
'ref' => $node,
'bid' => $acc['bid'],
'year' => $acc['year'],
], $acc['money']);
], $acc['money'], $dateFilter);
$output[] = $this->calculateCashdeskTotals($rows, $cashdesk, $node);
}
break;
@ -143,7 +144,7 @@ class ExploreAccountsController extends AbstractController
'ref' => $node,
'bid' => $acc['bid'],
'year' => $acc['year'],
], $acc['money']);
], $acc['money'], $dateFilter);
$output[] = $this->calculateSalaryTotals($rows, $salary, $node);
}
break;
@ -162,7 +163,7 @@ class ExploreAccountsController extends AbstractController
'ref' => $node,
'bid' => $acc['bid'],
'year' => $acc['year'],
], $acc['money']);
], $acc['money'], $dateFilter);
$output[] = $this->calculatePersonTotals($rows, $person, $node);
}
break;
@ -181,7 +182,7 @@ class ExploreAccountsController extends AbstractController
'ref' => $node,
'bid' => $acc['bid'],
'year' => $acc['year'],
], $acc['money']);
], $acc['money'], $dateFilter);
$output[] = $this->calculateCommodityTotals($rows, $commodity, $node);
}
break;
@ -200,7 +201,7 @@ class ExploreAccountsController extends AbstractController
'ref' => $node,
'bid' => $acc['bid'],
'year' => $acc['year'],
], $acc['money']);
], $acc['money'], $dateFilter);
$output[] = $this->calculateChequeTotals($rows, $cheque, $node);
}
break;
@ -241,6 +242,7 @@ class ExploreAccountsController extends AbstractController
$page = max(1, (int) ($params['page'] ?? 1));
$perPage = max(1, (int) ($params['perPage'] ?? 10));
$offset = ($page - 1) * $perPage;
$dateFilter = $params['dateFilter'] ?? null;
$rows = [];
$totalItems = 0;
@ -265,6 +267,14 @@ class ExploreAccountsController extends AbstractController
->setParameter('money', $acc['money'])
->setParameter('year', $acc['year']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$totalItems = (int) $qb->select('COUNT(r.id)')
->getQuery()
->getSingleScalarResult();
@ -298,6 +308,14 @@ class ExploreAccountsController extends AbstractController
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$totalItems = (int) $qb->select('COUNT(r.id)')
->getQuery()
->getSingleScalarResult();
@ -331,6 +349,14 @@ class ExploreAccountsController extends AbstractController
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$totalItems = (int) $qb->select('COUNT(r.id)')
->getQuery()
->getSingleScalarResult();
@ -364,6 +390,14 @@ class ExploreAccountsController extends AbstractController
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$totalItems = (int) $qb->select('COUNT(r.id)')
->getQuery()
->getSingleScalarResult();
@ -396,6 +430,14 @@ class ExploreAccountsController extends AbstractController
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$totalItems = (int) $qb->select('COUNT(r.id)')
->getQuery()
->getSingleScalarResult();
@ -428,6 +470,14 @@ class ExploreAccountsController extends AbstractController
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$totalItems = (int) $qb->select('COUNT(r.id)')
->getQuery()
->getSingleScalarResult();
@ -460,6 +510,14 @@ class ExploreAccountsController extends AbstractController
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$totalItems = (int) $qb->select('COUNT(r.id)')
->getQuery()
->getSingleScalarResult();
@ -500,6 +558,8 @@ class ExploreAccountsController extends AbstractController
throw $this->createNotFoundException('Required parameters (node, type, isObject) are missing');
}
$dateFilter = $params['dateFilter'] ?? null;
$node = $this->em->getRepository(HesabdariTable::class)
->findNode($params['upperID'] ?? $params['node'], $acc['bid']->getId());
if (!$node) {
@ -510,7 +570,7 @@ class ExploreAccountsController extends AbstractController
if ($params['isObject'] === false) {
$allNodes = $this->getAllDescendants($node, $acc);
$allNodes[] = $node;
$rows = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
$qb = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
->innerJoin('r.doc', 'd')
->where('r.ref IN (:nodeIds)')
->andWhere('r.bid = :bid OR r.bid IS NULL')
@ -519,9 +579,17 @@ class ExploreAccountsController extends AbstractController
->setParameter('nodeIds', array_map(fn($n) => $n->getId(), $allNodes))
->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money'])
->setParameter('year', $acc['year'])
->getQuery()
->getResult();
->setParameter('year', $acc['year']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$rows = $qb->getQuery()->getResult();
} else {
switch ($params['type']) {
case 'bank':
@ -533,7 +601,7 @@ class ExploreAccountsController extends AbstractController
if (!$item) {
throw $this->createNotFoundException('Bank account not found');
}
$rows = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
$qb = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
->innerJoin('r.doc', 'd')
->where('r.bank = :bank')
->andWhere('r.ref = :ref')
@ -544,9 +612,17 @@ class ExploreAccountsController extends AbstractController
->setParameter('ref', $node)
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money'])
->getQuery()
->getResult();
->setParameter('money', $acc['money']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$rows = $qb->getQuery()->getResult();
break;
case 'cashdesk':
@ -558,7 +634,7 @@ class ExploreAccountsController extends AbstractController
if (!$item) {
throw $this->createNotFoundException('Cashdesk not found');
}
$rows = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
$qb = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
->innerJoin('r.doc', 'd')
->where('r.cashdesk = :cashdesk')
->andWhere('r.ref = :ref')
@ -569,9 +645,17 @@ class ExploreAccountsController extends AbstractController
->setParameter('ref', $node)
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money'])
->getQuery()
->getResult();
->setParameter('money', $acc['money']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$rows = $qb->getQuery()->getResult();
break;
case 'salary':
@ -583,7 +667,7 @@ class ExploreAccountsController extends AbstractController
if (!$item) {
throw $this->createNotFoundException('Salary not found');
}
$rows = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
$qb = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
->innerJoin('r.doc', 'd')
->where('r.salary = :salary')
->andWhere('r.ref = :ref')
@ -594,9 +678,17 @@ class ExploreAccountsController extends AbstractController
->setParameter('ref', $node)
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money'])
->getQuery()
->getResult();
->setParameter('money', $acc['money']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$rows = $qb->getQuery()->getResult();
break;
case 'person':
@ -607,7 +699,7 @@ class ExploreAccountsController extends AbstractController
if (!$item) {
throw $this->createNotFoundException('Person not found');
}
$rows = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
$qb = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
->innerJoin('r.doc', 'd')
->where('r.person = :person')
->andWhere('r.ref = :ref')
@ -618,9 +710,17 @@ class ExploreAccountsController extends AbstractController
->setParameter('ref', $node)
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money'])
->getQuery()
->getResult();
->setParameter('money', $acc['money']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$rows = $qb->getQuery()->getResult();
break;
case 'commodity':
@ -631,7 +731,7 @@ class ExploreAccountsController extends AbstractController
if (!$item) {
throw $this->createNotFoundException('Commodity not found');
}
$rows = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
$qb = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
->innerJoin('r.doc', 'd')
->where('r.commodity = :commodity')
->andWhere('r.ref = :ref')
@ -642,9 +742,17 @@ class ExploreAccountsController extends AbstractController
->setParameter('ref', $node)
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money'])
->getQuery()
->getResult();
->setParameter('money', $acc['money']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$rows = $qb->getQuery()->getResult();
break;
case 'cheque':
@ -655,7 +763,7 @@ class ExploreAccountsController extends AbstractController
if (!$item) {
throw $this->createNotFoundException('Cheque not found');
}
$rows = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
$qb = $this->em->getRepository(HesabdariRow::class)->createQueryBuilder('r')
->innerJoin('r.doc', 'd')
->where('r.cheque = :cheque')
->andWhere('r.ref = :ref')
@ -666,9 +774,17 @@ class ExploreAccountsController extends AbstractController
->setParameter('ref', $node)
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money'])
->getQuery()
->getResult();
->setParameter('money', $acc['money']);
// اضافه کردن فیلتر تاریخ
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$qb->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
$rows = $qb->getQuery()->getResult();
break;
default:
@ -741,14 +857,14 @@ class ExploreAccountsController extends AbstractController
/**
* پیدا کردن ردیف‌های مرتبط با نودها (برای type=calc)
*/
private function getRowsForNodes(array $nodes, array $acc): array
private function getRowsForNodes(array $nodes, array $acc, ?array $dateFilter = null): array
{
$nodeIds = array_unique(array_map(fn($node) => $node->getId(), $nodes));
return $this->em->getRepository(HesabdariRow::class)->findByJoinMoney([
'ref' => $nodeIds,
'bid' => $acc['bid'],
'year' => $acc['year'],
], $acc['money']);
], $acc['money'], $dateFilter);
}
/**

View file

@ -102,7 +102,7 @@ class ShortlinksController extends AbstractController
}
#[Route('/slpdf/sell/{bid}/{link}', name: 'shortlinks_pdf')]
public function shortlinks_pdf(string $bid, string $link, EntityManagerInterface $entityManager, Provider $provider): Response
public function shortlinks_pdf(string $bid, string $link, EntityManagerInterface $entityManager, Provider $provider, \App\Service\PluginService $pluginService = null, \App\Service\TemplateRenderer $renderer = null): Response
{
$bus = $entityManager->getRepository(Business::class)->find($bid);
if (!$bus)
@ -122,6 +122,32 @@ class ShortlinksController extends AbstractController
]);
if (!$doc)
throw $this->createNotFoundException();
// تنظیم پارامترهای پیش‌فرض برای چاپ
$params = [
'printers' => false,
'pdf' => true,
'posPrint' => false
];
// دریافت تنظیمات پیش‌فرض از PrintOptions
$printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid' => $bid]);
// تنظیم مقادیر پیش‌فرض از تنظیمات ذخیره شده
$defaultOptions = [
'note' => $printSettings ? $printSettings->isSellNote() : true,
'bidInfo' => $printSettings ? $printSettings->isSellBidInfo() : true,
'taxInfo' => $printSettings ? $printSettings->isSellTaxInfo() : true,
'discountInfo' => $printSettings ? $printSettings->isSellDiscountInfo() : true,
'pays' => $printSettings ? $printSettings->isSellPays() : true,
'paper' => $printSettings ? $printSettings->getSellPaper() : 'A4-L',
'invoiceIndex' => $printSettings ? $printSettings->isSellInvoiceIndex() : true,
'businessStamp' => $printSettings ? $printSettings->isSellBusinessStamp() : true
];
// اولویت با پارامترهای ارسالی است
$printOptions = array_merge($defaultOptions, $params['printOptions'] ?? []);
$person = null;
$discount = 0;
$transfer = 0;
@ -134,35 +160,184 @@ class ShortlinksController extends AbstractController
$transfer = $item->getBs();
}
}
$printOptions = [
'bidInfo' => true,
'pays' => true,
'taxInfo' => true,
'discountInfo' => true,
'note' => true,
'paper' => 'A4-L'
];
$note = '';
$printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid' => $bid]);
if ($printSettings) {
$note = $printSettings->getSellNoteString();
$pdfPid = 0;
// فیلد جدید وضعیت حساب مشتری
$personItems = $entityManager->getRepository(HesabdariRow::class)->findBy(['bid' => $bid, 'person' => $person]);
$accountStatus = [];
$bs = 0;
$bd = 0;
foreach ($personItems as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
}
$pdfPid = $provider->createPrint(
$bid,
$bid->getOwner(),
$this->renderView('pdf/printers/sell.html.twig', [
'bid' => $bid,
'doc' => $doc,
'rows' => $doc->getHesabdariRows(),
'person' => $person,
if ($bs > $bd) {
$accountStatus['label'] = 'بستانکار';
$accountStatus['value'] = $bs - $bd;
} else {
$accountStatus['label'] = 'بدهکار';
$accountStatus['value'] = $bd - $bs;
}
$business = $entityManager->getRepository(Business::class)->find($bid);
$twoApproval = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
if ($twoApproval && $doc->isApproved() !== true && $doc->isPreview() == true) {
return $this->render('bundles/TwigBundle/Exception/error.html.twig', [
'message' => 'فاکتور هنوز تایید نشده است'
]);
}
// پیدا کردن مالک کسب و کار
$businessOwner = $bus->getOwner();
if ($params['pdf'] == true || $params['printers'] == true) {
$note = '';
if ($printSettings) {
$note = $printSettings->getSellNoteString();
}
// Build safe context data for rendering
$rowsArr = array_map(function ($row) {
return [
'commodity' => $row->getCommodity() ? [
'name' => method_exists($row->getCommodity(), 'getName') ? $row->getCommodity()->getName() : null,
'code' => method_exists($row->getCommodity(), 'getCode') ? $row->getCommodity()->getCode() : null,
] : null,
'commodityCount' => $row->getCommdityCount(),
'des' => $row->getDes(),
'bs' => $row->getBs(),
'tax' => $row->getTax(),
'discount' => $row->getDiscount(),
'showPercentDiscount' => $row->getDiscountType() === 'percent',
'discountPercent' => $row->getDiscountPercent()
];
}, $doc->getHesabdariRows()->toArray());
$personArr = $person ? [
'name' => $person->getName(),
'mobile' => $person->getMobile(),
'tel' => $person->getTel(),
'address' => $person->getAddress(),
] : null;
$businessArr = $bus ? [
'name' => method_exists($bus, 'getName') ? $bus->getName() : null,
'nikename' => method_exists($bus, 'getNikename') ? $bus->getNikename() : null,
'tel' => method_exists($bus, 'getTel') ? $bus->getTel() : null,
'mobile' => method_exists($bus, 'getMobile') ? $bus->getMobile() : null,
'address' => method_exists($bus, 'getAddress') ? $bus->getAddress() : null,
'shenasemeli' => method_exists($bus, 'getShenasemeli') ? $bus->getShenasemeli() : null,
'codeeghtesadi' => method_exists($bus, 'getCodeeghtesadi') ? $bus->getCodeeghtesadi() : null,
'id' => method_exists($bus, 'getId') ? $bus->getId() : null,
] : null;
$context = [
'accountStatus' => $accountStatus,
'business' => $businessArr,
'bid' => $businessArr,
'doc' => [
'code' => $doc->getCode(),
'date' => method_exists($doc, 'getDate') ? $doc->getDate() : null,
'taxPercent' => method_exists($doc, 'getTaxPercent') ? $doc->getTaxPercent() : null,
'discountPercent' => $doc->getDiscountPercent(),
'discountType' => $doc->getDiscountType(),
'amount' => $doc->getAmount(),
'money' => [
'shortName' => method_exists($doc, 'getMoney') && $doc->getMoney() && method_exists($doc->getMoney(), 'getShortName') ? $doc->getMoney()->getShortName() : null,
],
],
'rows' => $rowsArr,
'person' => $personArr,
'discount' => $discount,
'transfer' => $transfer,
'printOptions' => $printOptions,
'note' => $note
]),
false,
$printOptions['paper']
);
'note' => $note,
];
// Decide template: custom or default
$html = null;
// Check if custom invoice plugin is available and active
$isCustomInvoiceActive = false;
$selectedTemplate = null;
// Use injected services if available
if ($pluginService) {
$isCustomInvoiceActive = $pluginService->isActive('custominvoice', $bid);
$selectedTemplate = $printSettings ? $printSettings->getSellTemplate() : null;
}
if ($isCustomInvoiceActive && $selectedTemplate && class_exists('App\Entity\CustomInvoiceTemplate') && $selectedTemplate instanceof \App\Entity\CustomInvoiceTemplate) {
if ($renderer) {
$html = $renderer->render($selectedTemplate->getCode() ?? '', $context);
}
}
if ($html === null) {
// fallback to default Twig template
$html = $this->renderView('pdf/printers/sell.html.twig', [
'accountStatus' => $accountStatus,
'bid' => $bus,
'doc' => $doc,
'rows' => array_map(function ($row) {
return [
'commodity' => $row->getCommodity(),
'commodityCount' => $row->getCommdityCount(),
'des' => $row->getDes(),
'bs' => $row->getBs(),
'tax' => $row->getTax(),
'discount' => $row->getDiscount(),
'showPercentDiscount' => $row->getDiscountType() === 'percent',
'discountPercent' => $row->getDiscountPercent()
];
}, $doc->getHesabdariRows()->toArray()),
'person' => $person,
'printInvoice' => $params['printers'],
'discount' => $discount,
'transfer' => $transfer,
'printOptions' => $printOptions,
'note' => $note,
'showPercentDiscount' => $doc->getDiscountType() === 'percent',
'discountPercent' => $doc->getDiscountPercent()
]);
}
$pdfPid = $provider->createPrint(
$bus,
$businessOwner, // مالک کسب و کار
$html,
false,
$printOptions['paper']
);
}
if ($params['posPrint'] == true) {
$pid = $provider->createPrint(
$bus,
$businessOwner, // مالک کسب و کار
$this->renderView('pdf/posPrinters/justSell.html.twig', [
'bid' => $bus,
'doc' => $doc,
'rows' => array_map(function ($row) {
return [
'commodity' => $row->getCommodity(),
'commodityCount' => $row->getCommdityCount(),
'des' => $row->getDes(),
'bs' => $row->getBs(),
'tax' => $row->getTax(),
'discount' => $row->getDiscount(),
'showPercentDiscount' => $row->getDiscountType() === 'percent',
'discountPercent' => $row->getDiscountPercent()
];
}, $doc->getHesabdariRows()->toArray()),
'discount' => $discount,
'showPercentDiscount' => $doc->getDiscountType() === 'percent',
'discountPercent' => $doc->getDiscountPercent()
]),
false
);
}
return $this->redirectToRoute('app_front_print', ['id' => $pdfPid]);
}
}

View file

@ -52,12 +52,25 @@ class HesabdariController extends AbstractController
$acc = $access->hasRole('accounting');
if (!$acc)
throw $this->createAccessDeniedException();
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['code'],
'money' => $acc['money']
]);
// Check if we should include preview documents
$includePreview = $params['includePreview'] ?? false;
if ($includePreview) {
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['code'],
'money' => $acc['money']
]);
} else {
// Default: only approved documents
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['code'],
'money' => $acc['money']
]);
}
if (!$doc)
throw $this->createNotFoundException();
//add shortlink to doc
@ -208,6 +221,7 @@ class HesabdariController extends AbstractController
// Build base query
$queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('d.isPreview, d.isApproved')
->addSelect('u.fullName as submitter')
->from('App\Entity\HesabdariDoc', 'd')
->leftJoin('d.submitter', 'u')
@ -220,6 +234,21 @@ class HesabdariController extends AbstractController
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
// Apply approval filters - if not specified, only show approved documents
if (isset($filters['isApproved'])) {
$queryBuilder->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', $filters['isApproved']);
} else {
// Default: only show approved documents
$queryBuilder->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
if (isset($filters['isPreview'])) {
$queryBuilder->andWhere('d.isPreview = :isPreview')
->setParameter('isPreview', $filters['isPreview']);
}
// Add type filter if not 'all'
if ($type !== 'all') {
$queryBuilder->andWhere('d.type = :type')
@ -323,6 +352,8 @@ class HesabdariController extends AbstractController
'des' => $doc['des'],
'amount' => $doc['amount'],
'submitter' => $doc['submitter'],
'isPreview' => $doc['isPreview'],
'isApproved' => $doc['isApproved'],
];
// Get related person info if applicable
@ -443,6 +474,21 @@ class HesabdariController extends AbstractController
$doc->setSubmitter($this->getUser());
$doc->setMoney($acc['money']);
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
// Set approval status based on business settings
$business = $acc['bid'];
if ($business->isRequireTwoStepApproval()) {
// Two-step approval is enabled
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
} else {
// Two-step approval is disabled - auto approve
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
}
if (array_key_exists('refData', $params))
$doc->setRefData($params['refData']);
if (array_key_exists('plugin', $params))

View file

@ -7,6 +7,7 @@ use App\Entity\HesabdariRow;
use App\Entity\HesabdariTable;
use App\Service\Access;
use App\Service\Extractor;
use App\Service\Provider;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
@ -19,12 +20,14 @@ class HesabdariDocController extends AbstractController
private $em;
private $access;
private $extractor;
private $provider;
public function __construct(EntityManagerInterface $em, Access $access, Extractor $extractor)
public function __construct(EntityManagerInterface $em, Access $access, Extractor $extractor, Provider $provider)
{
$this->em = $em;
$this->access = $access;
$this->extractor = $extractor;
$this->provider = $provider;
}
#[Route('/hesabdari/tables', name: 'get_hesabdari_tables', methods: ['GET'])]
@ -103,7 +106,7 @@ class HesabdariDocController extends AbstractController
$doc->setDate($data['date']);
$doc->setDateSubmit((string) time());
$doc->setType('doc');
$doc->setCode($this->generateDocCode($accessData['bid']));
$doc->setCode($this->provider->getAccountingCode($accessData['bid'], 'accounting'));
$totalBd = 0;
$totalBs = 0;
@ -190,13 +193,5 @@ class HesabdariDocController extends AbstractController
return new JsonResponse($this->extractor->operationSuccess(['id' => $doc->getId()], 'سند با موفقیت ویرایش شد'));
}
private function generateDocCode($business): string
{
$lastDoc = $this->em->getRepository(HesabdariDoc::class)->findOneBy(
['bid' => $business],
['code' => 'DESC']
);
$newCode = $lastDoc ? ((int) $lastDoc->getCode() + 1) : 1;
return (string) $newCode;
}
}

File diff suppressed because it is too large Load diff

View file

@ -48,7 +48,7 @@ class IncomeController extends AbstractController
$yearStart = $jdate->jdate('Y/m/d', $yearStartUnix);
$yearEnd = $jdate->jdate('Y/m/d', $yearEndUnix);
// کوئری پایه - جمع bs را محاسبه می‌کنیم
// کوئری پایه - جمع bs را محاسبه می‌کنیم و فقط اسناد تایید شده
$qb = $entityManager->createQueryBuilder()
->select('SUM(COALESCE(r.bs, 0)) as total')
->from('App\Entity\HesabdariDoc', 'd')
@ -57,11 +57,13 @@ class IncomeController extends AbstractController
->andWhere('d.money = :money')
->andWhere('d.type = :type')
->andWhere('d.year = :year')
->andWhere('d.isApproved = :isApproved')
->andWhere('r.bs != 0') // فقط ردیف‌هایی که bs صفر نیست
->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money'])
->setParameter('type', 'income')
->setParameter('year', $acc['year']);
->setParameter('year', $acc['year'])
->setParameter('isApproved', true);
// درآمد امروز
$todayIncome = (clone $qb)
@ -123,7 +125,7 @@ class IncomeController extends AbstractController
$today = $jdate->jdate('Y/m/d', time());
$monthStart = $jdate->jdate('Y/m/01', time());
// کوئری پایه
// کوئری پایه - فقط اسناد تایید شده
$qb = $entityManager->createQueryBuilder()
->select('t.name AS center_name, SUM(COALESCE(r.bs, 0)) AS total_income')
->from('App\Entity\HesabdariDoc', 'd')
@ -133,13 +135,15 @@ class IncomeController extends AbstractController
->andWhere('d.money = :money')
->andWhere('d.type = :type')
->andWhere('d.year = :year')
->andWhere('d.isApproved = :isApproved')
->andWhere('r.bs != 0') // فقط ردیف‌هایی که bs صفر نیست
->groupBy('t.id, t.name')
->orderBy('total_income', 'DESC')
->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money'])
->setParameter('type', 'income')
->setParameter('year', $acc['year']);
->setParameter('year', $acc['year'])
->setParameter('isApproved', true);
// اعمال فیلتر تاریخ فقط برای امروز و ماه
if ($period === 'today') {
@ -200,6 +204,7 @@ class IncomeController extends AbstractController
// Build base query
$queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('d.isPreview, d.isApproved')
->addSelect('u.fullName as submitter')
->from('App\Entity\HesabdariDoc', 'd')
->leftJoin('d.submitter', 'u')
@ -214,6 +219,14 @@ class IncomeController extends AbstractController
->setParameter('type', $type)
->setParameter('money', $acc['money']);
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!$includePreview) {
// Default: only show approved documents
$queryBuilder->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
// Apply filters
if (!empty($filters)) {
// Text search
@ -310,6 +323,8 @@ class IncomeController extends AbstractController
'des' => $doc['des'],
'amount' => $doc['amount'],
'submitter' => $doc['submitter'],
'isPreview' => $doc['isPreview'],
'isApproved' => $doc['isApproved'],
];
// Get income center details
@ -375,14 +390,30 @@ class IncomeController extends AbstractController
$params = json_decode($request->getContent(), true) ?? [];
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
// دریافت آیتم‌های انتخاب شده یا همه آیتم‌ها
if (!isset($params['items'])) {
$items = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'type' => 'income',
'year' => $acc['year'],
'money' => $acc['money']
]);
$query = $entityManager->createQueryBuilder()
->select('d')
->from(HesabdariDoc::class, 'd')
->where('d.bid = :bid')
->andWhere('d.type = :type')
->andWhere('d.year = :year')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('type', 'income')
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$items = $query->getQuery()->getResult();
} else {
$items = [];
foreach ($params['items'] as $param) {
@ -394,7 +425,10 @@ class IncomeController extends AbstractController
'money' => $acc['money']
]);
if ($doc) {
$items[] = $doc;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $doc->isApproved()) {
$items[] = $doc;
}
}
}
}
@ -426,14 +460,30 @@ class IncomeController extends AbstractController
$params = json_decode($request->getContent(), true) ?? [];
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
// دریافت آیتم‌های انتخاب شده یا همه آیتم‌ها
if (!isset($params['items'])) {
$items = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'type' => 'income',
'year' => $acc['year'],
'money' => $acc['money']
]);
$query = $entityManager->createQueryBuilder()
->select('d')
->from(HesabdariDoc::class, 'd')
->where('d.bid = :bid')
->andWhere('d.type = :type')
->andWhere('d.year = :year')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('type', 'income')
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$items = $query->getQuery()->getResult();
} else {
$items = [];
foreach ($params['items'] as $param) {
@ -445,7 +495,10 @@ class IncomeController extends AbstractController
'money' => $acc['money']
]);
if ($doc) {
$items[] = $doc;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $doc->isApproved()) {
$items[] = $doc;
}
}
}
}
@ -563,6 +616,20 @@ class IncomeController extends AbstractController
$doc->setMoney($acc['money']);
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
// Set approval status based on business settings
$business = $acc['bid'];
if ($business->isRequireTwoStepApproval()) {
// Two-step approval is enabled
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
} else {
// Two-step approval is disabled - auto approve
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
}
$entityManager->persist($doc);
$entityManager->flush();

View file

@ -1,18 +0,0 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class MoadiyanController extends AbstractController
{
#[Route('api/moadiyan', name: 'app_moadiyan')]
public function index(): Response
{
return $this->render('moadiyan/index.html.twig', [
'controller_name' => 'MoadiyanController',
]);
}
}

View file

@ -0,0 +1,453 @@
<?php
namespace App\Controller;
use App\Entity\OAuthApplication;
use App\Entity\OAuthScope;
use App\Service\OAuthService;
use App\Service\Extractor;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use App\Service\Provider;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
#[Route('/api/admin/oauth')]
class OAuthApplicationController extends AbstractController
{
private OAuthService $oauthService;
private Extractor $extractor;
private LoggerInterface $logger;
private Provider $provider;
private EntityManagerInterface $entityManager;
private ValidatorInterface $validator;
public function __construct(
OAuthService $oauthService,
Extractor $extractor,
#[Autowire('@monolog.logger.oauth')] LoggerInterface $logger,
Provider $provider,
EntityManagerInterface $entityManager,
ValidatorInterface $validator
) {
$this->oauthService = $oauthService;
$this->extractor = $extractor;
$this->logger = $logger;
$this->provider = $provider;
$this->entityManager = $entityManager;
$this->validator = $validator;
}
/**
* لیست برنامه‌های OAuth کاربر
*/
#[Route('/applications', name: 'api_admin_oauth_applications_list', methods: ['GET'])]
public function listApplications(): JsonResponse
{
$user = $this->getUser();
if (!$user) {
throw $this->createAccessDeniedException();
}
$applications = $this->entityManager->getRepository(\App\Entity\OAuthApplication::class)->findByOwner($user->getId());
return $this->json($this->extractor->operationSuccess(
$this->provider->ArrayEntity2Array($applications, 1, ['owner', 'scopes'])
));
}
/**
* ایجاد برنامه OAuth جدید
*/
#[Route('/applications', name: 'api_admin_oauth_applications_create', methods: ['POST'])]
public function createApplication(Request $request): JsonResponse
{
$user = $this->getUser();
if (!$user) {
throw $this->createAccessDeniedException();
}
$data = json_decode($request->getContent(), true);
// اعتبارسنجی داده‌های ورودی
if (!isset($data['name'])) {
return $this->json($this->extractor->operationFail('نام الزامی است'));
}
// پشتیبانی از هر دو فرمت redirect_uri و redirectUri
$redirectUri = $data['redirect_uri'] ?? $data['redirectUri'] ?? null;
if (!$redirectUri) {
return $this->json($this->extractor->operationFail('آدرس بازگشت الزامی است'));
}
// بررسی تکراری نبودن نام
$existingApp = $this->entityManager->getRepository(OAuthApplication::class)->findOneBy([
'name' => $data['name'],
'owner' => $user
]);
if ($existingApp) {
return $this->json($this->extractor->operationFail('برنامه‌ای با این نام قبلاً وجود دارد'));
}
// ایجاد برنامه جدید
$application = new OAuthApplication();
$application->setName($data['name']);
$application->setDescription($data['description'] ?? '');
$application->setWebsite($data['website'] ?? '');
$application->setRedirectUri($redirectUri);
$application->setOwner($user);
// تنظیم فیلدهای اختیاری
if (isset($data['rateLimit']) || isset($data['rate_limit'])) {
$rateLimit = $data['rateLimit'] ?? $data['rate_limit'] ?? 1000;
$application->setRateLimit($rateLimit);
}
if (isset($data['allowedScopes']) || isset($data['allowed_scopes'])) {
$allowedScopes = $data['allowedScopes'] ?? $data['allowed_scopes'] ?? [];
$application->setAllowedScopes($allowedScopes);
}
if (isset($data['ipWhitelist']) || isset($data['ip_whitelist'])) {
$ipWhitelist = $data['ipWhitelist'] ?? $data['ip_whitelist'] ?? [];
$application->setIpWhitelist($ipWhitelist);
}
// تولید client_id و client_secret
$credentials = $this->oauthService->generateClientCredentials();
$application->setClientId($credentials['client_id']);
$application->setClientSecret($credentials['client_secret']);
// تنظیم محدوده‌های پیش‌فرض (فقط اگر محدوده‌ای تنظیم نشده باشد)
if (empty($application->getAllowedScopes())) {
$defaultScopes = $this->entityManager->getRepository(OAuthScope::class)->findDefaultScopes();
$application->setAllowedScopes(array_map(fn($scope) => $scope->getName(), $defaultScopes));
}
// اعتبارسنجی
$errors = $this->validator->validate($application);
if (count($errors) > 0) {
return $this->json($this->extractor->operationFail('داده‌های ورودی نامعتبر است'));
}
$this->entityManager->persist($application);
$this->entityManager->flush();
// ثبت لاگ
$this->logger->info('OAuth Application Created', [
'application_name' => $application->getName(),
'user_id' => $user->getId(),
'user_email' => $user->getEmail()
]);
return $this->json($this->extractor->operationSuccess([
'application' => $this->provider->Entity2Array($application, 1, ['owner', 'scopes']),
'client_id' => $application->getClientId(),
'client_secret' => $application->getClientSecret()
]));
}
/**
* ویرایش برنامه OAuth
*/
#[Route('/applications/{id}', name: 'api_admin_oauth_applications_update', methods: ['PUT'])]
public function updateApplication(Request $request, int $id): JsonResponse
{
$user = $this->getUser();
if (!$user) {
throw $this->createAccessDeniedException();
}
$application = $this->entityManager->getRepository(OAuthApplication::class)->find($id);
if (!$application || $application->getOwner()->getId() !== $user->getId()) {
throw $this->createNotFoundException('برنامه یافت نشد');
}
$data = json_decode($request->getContent(), true);
if (isset($data['name'])) {
$application->setName($data['name']);
}
if (isset($data['description'])) {
$application->setDescription($data['description']);
}
if (isset($data['website'])) {
$application->setWebsite($data['website']);
}
if (isset($data['redirect_uri']) || isset($data['redirectUri'])) {
$redirectUri = $data['redirect_uri'] ?? $data['redirectUri'];
$application->setRedirectUri($redirectUri);
}
if (isset($data['allowed_scopes']) || isset($data['allowedScopes'])) {
$allowedScopes = $data['allowed_scopes'] ?? $data['allowedScopes'];
$application->setAllowedScopes($allowedScopes);
}
if (isset($data['rate_limit']) || isset($data['rateLimit'])) {
$rateLimit = $data['rate_limit'] ?? $data['rateLimit'];
$application->setRateLimit($rateLimit);
}
if (isset($data['ip_whitelist']) || isset($data['ipWhitelist'])) {
$ipWhitelist = $data['ip_whitelist'] ?? $data['ipWhitelist'] ?? [];
$application->setIpWhitelist($ipWhitelist);
}
// اعتبارسنجی
$errors = $this->validator->validate($application);
if (count($errors) > 0) {
return $this->json($this->extractor->operationFail('داده‌های ورودی نامعتبر است'));
}
$this->entityManager->flush();
// ثبت لاگ
$this->logger->info('OAuth Application Updated', [
'application_name' => $application->getName(),
'user_id' => $user->getId(),
'user_email' => $user->getEmail()
]);
return $this->json($this->extractor->operationSuccess(
$this->provider->Entity2Array($application, 1, ['owner', 'scopes'])
));
}
/**
* حذف برنامه OAuth
*/
#[Route('/applications/{id}', name: 'api_admin_oauth_applications_delete', methods: ['DELETE'])]
public function deleteApplication(int $id): JsonResponse
{
$user = $this->getUser();
if (!$user) {
throw $this->createAccessDeniedException();
}
$application = $this->entityManager->getRepository(OAuthApplication::class)->find($id);
if (!$application || $application->getOwner()->getId() !== $user->getId()) {
throw $this->createNotFoundException('برنامه یافت نشد');
}
$appName = $application->getName();
// لغو تمام توکن‌های مربوط به این برنامه
$this->entityManager->getRepository(\App\Entity\OAuthAccessToken::class)->revokeApplicationTokens($application->getId());
$this->entityManager->remove($application);
$this->entityManager->flush();
// ثبت لاگ
$this->logger->info('OAuth Application Deleted', [
'application_name' => $appName,
'user_id' => $user->getId(),
'user_email' => $user->getEmail()
]);
return $this->json($this->extractor->operationSuccess());
}
/**
* بازسازی client_secret
*/
#[Route('/applications/{id}/regenerate-secret', name: 'api_admin_oauth_applications_regenerate_secret', methods: ['POST'])]
public function regenerateClientSecret(int $id): JsonResponse
{
$user = $this->getUser();
if (!$user) {
throw $this->createAccessDeniedException();
}
$application = $this->entityManager->getRepository(OAuthApplication::class)->find($id);
if (!$application || $application->getOwner()->getId() !== $user->getId()) {
throw $this->createNotFoundException('برنامه یافت نشد');
}
// لغو تمام توکن‌های موجود
$this->entityManager->getRepository(\App\Entity\OAuthAccessToken::class)->revokeApplicationTokens($application->getId());
// تولید client_secret جدید
$credentials = $this->oauthService->generateClientCredentials();
$application->setClientSecret($credentials['client_secret']);
$this->entityManager->flush();
// ثبت لاگ
$this->logger->info('OAuth Client Secret Regenerated', [
'application_name' => $application->getName(),
'user_id' => $user->getId(),
'user_email' => $user->getEmail()
]);
return $this->json($this->extractor->operationSuccess([
'client_secret' => $application->getClientSecret()
]));
}
/**
* لیست محدوده‌های دسترسی
*/
#[Route('/scopes', name: 'api_admin_oauth_scopes_list', methods: ['GET'])]
public function listScopes(): JsonResponse
{
$scopes = $this->entityManager->getRepository(OAuthScope::class)->findAll();
return $this->json($this->extractor->operationSuccess(
$this->provider->ArrayEntity2Array($scopes)
));
}
/**
* آمار استفاده از برنامه OAuth
*/
#[Route('/applications/{id}/stats', name: 'api_admin_oauth_applications_stats', methods: ['GET'])]
public function getApplicationStats(int $id): JsonResponse
{
$user = $this->getUser();
if (!$user) {
throw $this->createAccessDeniedException();
}
$application = $this->entityManager->getRepository(OAuthApplication::class)->find($id);
if (!$application || $application->getOwner()->getId() !== $user->getId()) {
throw $this->createNotFoundException('برنامه یافت نشد');
}
// تعداد توکن‌های فعال
$activeTokens = $this->entityManager->getRepository(\App\Entity\OAuthAccessToken::class)->findByApplication($application->getId());
$activeTokensCount = count(array_filter($activeTokens, fn($token) => $token->isValid()));
// تعداد توکن‌های منقضی شده
$expiredTokensCount = count(array_filter($activeTokens, fn($token) => $token->isExpired()));
// آخرین استفاده
$lastUsed = null;
if (!empty($activeTokens)) {
$lastUsedToken = max($activeTokens, fn($a, $b) => $a->getLastUsedAt() <=> $b->getLastUsedAt());
$lastUsed = $lastUsedToken->getLastUsedAt();
}
$stats = [
'total_tokens' => count($activeTokens),
'active_tokens' => $activeTokensCount,
'expired_tokens' => $expiredTokensCount,
'last_used' => $lastUsed,
'created_at' => $application->getCreatedAt(),
'is_active' => $application->isActive()
];
return $this->json($this->extractor->operationSuccess($stats));
}
/**
* لغو تمام توکن‌های برنامه
*/
#[Route('/applications/{id}/revoke-tokens', name: 'api_admin_oauth_applications_revoke_tokens', methods: ['POST'])]
public function revokeAllTokens(int $id): JsonResponse
{
$user = $this->getUser();
if (!$user) {
throw $this->createAccessDeniedException();
}
$application = $this->entityManager->getRepository(OAuthApplication::class)->find($id);
if (!$application || $application->getOwner()->getId() !== $user->getId()) {
throw $this->createNotFoundException('برنامه یافت نشد');
}
$revokedCount = $this->entityManager->getRepository(\App\Entity\OAuthAccessToken::class)->revokeApplicationTokens($application->getId());
// ثبت لاگ
$this->logger->info('OAuth Tokens Revoked', [
'application_name' => $application->getName(),
'revoked_count' => $revokedCount,
'user_id' => $user->getId(),
'user_email' => $user->getEmail()
]);
return $this->json($this->extractor->operationSuccess([
'revoked_count' => $revokedCount
]));
}
/**
* دریافت اطلاعات برنامه بر اساس Client ID
*/
#[Route('/applications/client/{clientId}', name: 'api_admin_oauth_applications_by_client_id', methods: ['GET'])]
public function getApplicationByClientId(string $clientId): JsonResponse
{
try {
$application = $this->entityManager->getRepository(\App\Entity\OAuthApplication::class)->findByClientId($clientId);
if (!$application) {
return $this->json([
'Success' => false,
'message' => 'برنامه یافت نشد'
], Response::HTTP_NOT_FOUND);
}
return $this->json([
'Success' => true,
'data' => [
'id' => $application->getId(),
'name' => $application->getName(),
'description' => $application->getDescription(),
'website' => $application->getWebsite(),
'redirectUri' => $application->getRedirectUri(),
'clientId' => $application->getClientId(),
'isActive' => $application->isActive(),
'allowedScopes' => $application->getAllowedScopes(),
'createdAt' => $application->getCreatedAt()
]
]);
} catch (\Exception $e) {
$this->logger->error('Error getting application by client ID: ' . $e->getMessage());
return $this->json([
'Success' => false,
'message' => 'خطا در دریافت اطلاعات برنامه'
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* فعال/غیرفعال کردن برنامه OAuth
*/
#[Route('/applications/{id}/toggle-status', name: 'api_admin_oauth_applications_toggle_status', methods: ['POST'])]
public function toggleApplicationStatus(int $id): JsonResponse
{
$user = $this->getUser();
if (!$user) {
throw $this->createAccessDeniedException();
}
$application = $this->entityManager->getRepository(OAuthApplication::class)->find($id);
if (!$application || $application->getOwner()->getId() !== $user->getId()) {
throw $this->createNotFoundException('برنامه یافت نشد');
}
$oldStatus = $application->isActive();
$newStatus = !$oldStatus;
$application->setIsActive($newStatus);
$this->entityManager->flush();
// ثبت لاگ
$this->logger->info('OAuth Application Status Changed', [
'application_name' => $application->getName(),
'old_status' => $oldStatus ? 'active' : 'inactive',
'new_status' => $newStatus ? 'active' : 'inactive',
'user_id' => $user->getId(),
'user_email' => $user->getEmail()
]);
return $this->json($this->extractor->operationSuccess([
'is_active' => $newStatus,
'message' => $newStatus ? 'برنامه فعال شد' : 'برنامه غیرفعال شد'
]));
}
}

View file

@ -0,0 +1,346 @@
<?php
namespace App\Controller;
use App\Entity\OAuthApplication;
use App\Entity\OAuthAuthorizationCode;
use App\Entity\OAuthAccessToken;
use App\Service\OAuthService;
use App\Service\Extractor;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
#[Route('/oauth')]
class OAuthController extends AbstractController
{
private OAuthService $oauthService;
private Extractor $extractor;
public function __construct(OAuthService $oauthService, Extractor $extractor)
{
$this->oauthService = $oauthService;
$this->extractor = $extractor;
}
/**
* Authorization endpoint - مرحله اول OAuth flow
*/
#[Route('/authorize', name: 'oauth_authorize', methods: ['GET'])]
public function authorize(Request $request): Response
{
try {
$validation = $this->oauthService->validateAuthorizationRequest($request);
$application = $validation['application'];
$scopes = $validation['scopes'];
$state = $validation['state'];
// هدایت به صفحه frontend
$frontendUrl = $this->getParameter('app.frontend_url') . '/oauth/authorize?' . $request->getQueryString();
return $this->redirect($frontendUrl);
} catch (AuthenticationException $e) {
return new JsonResponse([
'error' => 'invalid_request',
'error_description' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
/**
* API endpoint برای frontend - تایید مجوز
*/
#[Route('/api/oauth/authorize', name: 'api_oauth_authorize', methods: ['POST'])]
public function authorizeApi(Request $request): JsonResponse
{
try {
$data = json_decode($request->getContent(), true);
$clientId = $data['client_id'] ?? null;
$redirectUri = $data['redirect_uri'] ?? null;
$scope = $data['scope'] ?? null;
$state = $data['state'] ?? null;
$approved = $data['approved'] ?? false;
if (!$this->getUser()) {
return $this->json([
'Success' => false,
'message' => 'کاربر احراز هویت نشده'
], Response::HTTP_UNAUTHORIZED);
}
if (!$approved) {
// کاربر مجوز را رد کرده
$errorParams = [
'error' => 'access_denied',
'error_description' => 'User denied access'
];
if ($state) {
$errorParams['state'] = $state;
}
$redirectUrl = $redirectUri . '?' . http_build_query($errorParams);
return $this->json([
'Success' => true,
'redirect_url' => $redirectUrl
]);
}
$application = $this->oauthService->getApplicationRepository()->findByClientId($clientId);
if (!$application) {
return $this->json([
'Success' => false,
'message' => 'برنامه نامعتبر'
], Response::HTTP_BAD_REQUEST);
}
$scopes = $scope ? explode(' ', $scope) : [];
// ایجاد کد مجوز
$authorizationCode = $this->oauthService->createAuthorizationCode(
$this->getUser(),
$application,
$scopes,
$state
);
// هدایت به redirect_uri با کد مجوز
$params = [
'code' => $authorizationCode->getCode()
];
if ($state) {
$params['state'] = $state;
}
$redirectUrl = $redirectUri . '?' . http_build_query($params);
return $this->json([
'Success' => true,
'redirect_url' => $redirectUrl
]);
} catch (\Exception $e) {
$errorParams = [
'error' => 'server_error',
'error_description' => $e->getMessage()
];
if ($state) {
$errorParams['state'] = $state;
}
$redirectUrl = $redirectUri . '?' . http_build_query($errorParams);
return $this->json([
'Success' => true,
'redirect_url' => $redirectUrl
]);
}
}
/**
* Token endpoint - مرحله دوم OAuth flow
*/
#[Route('/token', name: 'oauth_token', methods: ['POST'])]
public function token(Request $request): JsonResponse
{
$grantType = $request->request->get('grant_type');
$clientId = $request->request->get('client_id');
$clientSecret = $request->request->get('client_secret');
// اعتبارسنجی client credentials
$application = $this->oauthService->getApplicationRepository()->findByClientId($clientId);
if (!$application || $application->getClientSecret() !== $clientSecret) {
return $this->json([
'error' => 'invalid_client',
'error_description' => 'Invalid client credentials'
], Response::HTTP_UNAUTHORIZED);
}
switch ($grantType) {
case 'authorization_code':
return $this->handleAuthorizationCodeGrant($request, $application);
case 'refresh_token':
return $this->handleRefreshTokenGrant($request, $application);
default:
return $this->json([
'error' => 'unsupported_grant_type',
'error_description' => 'Unsupported grant type'
], Response::HTTP_BAD_REQUEST);
}
}
/**
* مدیریت Authorization Code Grant
*/
private function handleAuthorizationCodeGrant(Request $request, OAuthApplication $application): JsonResponse
{
$code = $request->request->get('code');
$redirectUri = $request->request->get('redirect_uri');
if (!$code || !$redirectUri) {
return $this->json([
'error' => 'invalid_request',
'error_description' => 'Missing required parameters'
], Response::HTTP_BAD_REQUEST);
}
$authorizationCode = $this->oauthService->validateAuthorizationCode($code, $application->getClientId(), $redirectUri);
if (!$authorizationCode) {
return $this->json([
'error' => 'invalid_grant',
'error_description' => 'Invalid authorization code'
], Response::HTTP_BAD_REQUEST);
}
// استفاده از کد مجوز
$this->oauthService->useAuthorizationCode($authorizationCode);
// ایجاد توکن دسترسی
$accessToken = $this->oauthService->createAccessToken(
$authorizationCode->getUser(),
$application,
$authorizationCode->getScopes()
);
return $this->json([
'access_token' => $accessToken->getToken(),
'token_type' => 'Bearer',
'expires_in' => 3600, // 1 hour
'refresh_token' => $accessToken->getRefreshToken(),
'scope' => implode(' ', $accessToken->getScopes())
]);
}
/**
* مدیریت Refresh Token Grant
*/
private function handleRefreshTokenGrant(Request $request, OAuthApplication $application): JsonResponse
{
$refreshToken = $request->request->get('refresh_token');
if (!$refreshToken) {
return $this->json([
'error' => 'invalid_request',
'error_description' => 'Missing refresh_token'
], Response::HTTP_BAD_REQUEST);
}
$accessToken = $this->oauthService->getAccessTokenRepository()->findByRefreshToken($refreshToken);
if (!$accessToken || $accessToken->getApplication()->getId() !== $application->getId()) {
return $this->json([
'error' => 'invalid_grant',
'error_description' => 'Invalid refresh token'
], Response::HTTP_BAD_REQUEST);
}
// ایجاد توکن جدید
$newAccessToken = $this->oauthService->createAccessToken(
$accessToken->getUser(),
$application,
$accessToken->getScopes()
);
// لغو توکن قدیمی
$accessToken->setIsRevoked(true);
$this->oauthService->getEntityManager()->flush();
return $this->json([
'access_token' => $newAccessToken->getToken(),
'token_type' => 'Bearer',
'expires_in' => 3600,
'refresh_token' => $newAccessToken->getRefreshToken(),
'scope' => implode(' ', $newAccessToken->getScopes())
]);
}
/**
* User Info endpoint
*/
#[Route('/userinfo', name: 'oauth_userinfo', methods: ['GET'])]
public function userinfo(Request $request): JsonResponse
{
$user = $this->getUser();
if (!$user) {
return $this->json([
'error' => 'invalid_token',
'error_description' => 'Invalid access token'
], Response::HTTP_UNAUTHORIZED);
}
return $this->json([
'id' => $user->getId(),
'email' => $user->getEmail(),
'name' => $user->getName(),
'profile' => [
'phone' => $user->getMobile(),
'address' => $user->getAddress()
]
]);
}
/**
* Revoke endpoint
*/
#[Route('/revoke', name: 'oauth_revoke', methods: ['POST'])]
public function revoke(Request $request): JsonResponse
{
$token = $request->request->get('token');
$tokenTypeHint = $request->request->get('token_type_hint', 'access_token');
if (!$token) {
return $this->json([
'error' => 'invalid_request',
'error_description' => 'Missing token'
], Response::HTTP_BAD_REQUEST);
}
$success = false;
if ($tokenTypeHint === 'access_token') {
$success = $this->oauthService->revokeAccessToken($token);
} elseif ($tokenTypeHint === 'refresh_token') {
$accessToken = $this->oauthService->getAccessTokenRepository()->findByRefreshToken($token);
if ($accessToken) {
$accessToken->setIsRevoked(true);
$this->oauthService->getEntityManager()->flush();
$success = true;
}
}
return $this->json(['success' => $success]);
}
/**
* اطلاعات برنامه OAuth
*/
#[Route('/.well-known/oauth-authorization-server', name: 'oauth_discovery', methods: ['GET'])]
public function discovery(): JsonResponse
{
return $this->json([
'issuer' => $this->getParameter('app.site_url'),
'authorization_endpoint' => $this->generateUrl('oauth_authorize', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL),
'token_endpoint' => $this->generateUrl('oauth_token', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL),
'userinfo_endpoint' => $this->generateUrl('oauth_userinfo', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL),
'revocation_endpoint' => $this->generateUrl('oauth_revoke', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL),
'response_types_supported' => ['code'],
'grant_types_supported' => ['authorization_code', 'refresh_token'],
'token_endpoint_auth_methods_supported' => ['client_secret_post'],
'scopes_supported' => [
'read_profile',
'write_profile',
'read_business',
'write_business',
'read_financial',
'write_financial',
'read_contacts',
'write_contacts',
'read_documents',
'write_documents',
'admin_access'
]
]);
}
}

View file

@ -136,6 +136,37 @@ class OpenbalanceController extends AbstractController
if (!$acc)
throw $this->createAccessDeniedException();
// بررسی مجوز تغییر تراز افتتاحیه
$years = $entityManagerInterface->getRepository(\App\Entity\Year::class)->findBy([
'bid' => $acc['bid']
], ['start' => 'ASC']);
$currentYear = $acc['year'];
$isFirstYear = false;
$hasMultipleYears = count($years) > 1;
// بررسی اینکه آیا سال فعلی اولین سال مالی است
if (count($years) > 0) {
$firstYear = $years[0];
$isFirstYear = ($currentYear->getId() === $firstYear->getId());
}
$canModify = $isFirstYear && !$hasMultipleYears;
if (!$canModify) {
$message = '';
if ($hasMultipleYears && !$isFirstYear) {
$message = 'تراز افتتاحیه فقط مختص سال مالی اول است. برای سال‌های بعدی از بستن سال مالی استفاده کنید.';
} elseif ($hasMultipleYears && $isFirstYear) {
$message = 'این کسب و کار دارای چندین سال مالی است. تراز افتتاحیه فقط در سال اول قابل تغییر است.';
}
return $this->json([
'result' => 0,
'message' => $message
]);
}
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
@ -157,7 +188,7 @@ class OpenbalanceController extends AbstractController
$doc->setSubmitter($this->getUser());
$doc->setYear($acc['year']);
$doc->setDes('سند افتتاحیه');
$doc->setDate($jdate->jdate('Y/n/d', time()));
$doc->setDate($jdate->jdate('Y/n/d', $acc['year']->getStart()));
$doc->setType('open_balance');
$doc->setCode($provider->getAccountingCode($acc['bid'],'accounting'));
$entityManagerInterface->persist($doc);
@ -233,7 +264,7 @@ class OpenbalanceController extends AbstractController
$doc->setSubmitter($this->getUser());
$doc->setYear($acc['year']);
$doc->setDes('سند افتتاحیه');
$doc->setDate($jdate->jdate('Y/n/d', time()));
$doc->setDate($jdate->jdate('Y/n/d', $acc['year']->getStart()));
$doc->setType('open_balance');
$doc->setCode($provider->getAccountingCode($acc['bid'],'accounting'));
$entityManagerInterface->persist($doc);
@ -309,7 +340,7 @@ class OpenbalanceController extends AbstractController
$doc->setSubmitter($this->getUser());
$doc->setYear($acc['year']);
$doc->setDes('سند افتتاحیه');
$doc->setDate($jdate->jdate('Y/n/d', time()));
$doc->setDate($jdate->jdate('Y/n/d', $acc['year']->getStart()));
$doc->setType('open_balance');
$doc->setCode($provider->getAccountingCode($acc['bid'],'accounting'));
$entityManagerInterface->persist($doc);
@ -385,7 +416,7 @@ class OpenbalanceController extends AbstractController
$doc->setSubmitter($this->getUser());
$doc->setYear($acc['year']);
$doc->setDes('سند افتتاحیه');
$doc->setDate($jdate->jdate('Y/n/d', time()));
$doc->setDate($jdate->jdate('Y/n/d', $acc['year']->getStart()));
$doc->setType('open_balance');
$doc->setCode($provider->getAccountingCode($acc['bid'],'accounting'));
$entityManagerInterface->persist($doc);
@ -468,7 +499,7 @@ class OpenbalanceController extends AbstractController
$doc->setSubmitter($this->getUser());
$doc->setYear($acc['year']);
$doc->setDes('سند افتتاحیه');
$doc->setDate($jdate->jdate('Y/n/d', time()));
$doc->setDate($jdate->jdate('Y/n/d', $acc['year']->getStart()));
$doc->setType('open_balance');
$doc->setCode($provider->getAccountingCode($acc['bid'],'accounting'));
$entityManagerInterface->persist($doc);
@ -533,4 +564,93 @@ class OpenbalanceController extends AbstractController
$entityManagerInterface->flush();
return $this->json($extractor->operationSuccess());
}
#[Route('/api/openbalance/check-permission', name: 'app_openbalance_check_permission')]
public function app_openbalance_check_permission(Access $access, EntityManagerInterface $entityManagerInterface, Extractor $extractor): Response
{
$acc = $access->hasRole('accounting');
if (!$acc)
throw $this->createAccessDeniedException();
// بررسی تعداد سال‌های مالی کسب و کار
$years = $entityManagerInterface->getRepository(\App\Entity\Year::class)->findBy([
'bid' => $acc['bid']
], ['start' => 'ASC']);
$currentYear = $acc['year'];
$isFirstYear = false;
$hasMultipleYears = count($years) > 1;
// بررسی اینکه آیا سال فعلی اولین سال مالی است
if (count($years) > 0) {
$firstYear = $years[0];
$isFirstYear = ($currentYear->getId() === $firstYear->getId());
}
// بررسی اینکه آیا سند افتتاحیه قبلاً ایجاد شده
$existingDoc = $entityManagerInterface->getRepository(HesabdariDoc::class)->findOneBy([
'year' => $currentYear,
'bid' => $acc['bid'],
'type' => 'open_balance',
'money' => $acc['money']
]);
$canModify = $isFirstYear && !$hasMultipleYears;
$message = '';
if ($hasMultipleYears && !$isFirstYear) {
$message = 'تراز افتتاحیه فقط مختص سال مالی اول است. برای سال‌های بعدی از بستن سال مالی استفاده کنید.';
} elseif ($hasMultipleYears && $isFirstYear) {
$message = 'این کسب و کار دارای چندین سال مالی است. تراز افتتاحیه فقط در سال اول قابل تغییر است.';
} elseif ($existingDoc) {
$message = 'سند افتتاحیه قبلاً ایجاد شده است.';
}
return $this->json([
'canModify' => $canModify,
'isFirstYear' => $isFirstYear,
'hasMultipleYears' => $hasMultipleYears,
'existingDoc' => $existingDoc ? true : false,
'message' => $message,
'yearsCount' => count($years)
]);
}
/**
* بررسی مجوز تغییر تراز افتتاحیه
*/
private function checkOpeningBalancePermission(EntityManagerInterface $entityManagerInterface, array $acc): array
{
// بررسی تعداد سال‌های مالی کسب و کار
$years = $entityManagerInterface->getRepository(\App\Entity\Year::class)->findBy([
'bid' => $acc['bid']
], ['start' => 'ASC']);
$currentYear = $acc['year'];
$isFirstYear = false;
$hasMultipleYears = count($years) > 1;
// بررسی اینکه آیا سال فعلی اولین سال مالی است
if (count($years) > 0) {
$firstYear = $years[0];
$isFirstYear = ($currentYear->getId() === $firstYear->getId());
}
$canModify = $isFirstYear && !$hasMultipleYears;
$message = '';
if ($hasMultipleYears && !$isFirstYear) {
$message = 'تراز افتتاحیه فقط مختص سال مالی اول است. برای سال‌های بعدی از بستن سال مالی استفاده کنید.';
} elseif ($hasMultipleYears && $isFirstYear) {
$message = 'این کسب و کار دارای چندین سال مالی است. تراز افتتاحیه فقط در سال اول قابل تغییر است.';
}
return [
'canModify' => $canModify,
'message' => $message,
'isFirstYear' => $isFirstYear,
'hasMultipleYears' => $hasMultipleYears,
'yearsCount' => count($years)
];
}
}

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,9 @@ namespace App\Controller;
use App\Entity\Plugin;
use App\Entity\PluginProdect;
use App\Entity\Business;
use App\Service\Access;
use App\Service\PluginService;
use App\Service\Extractor;
use App\Service\Jdate;
use App\Service\Log;
@ -23,6 +25,23 @@ class PluginController extends AbstractController
{
private const PRICE_MULTIPLIER = 10; // ضریب قیمت به صورت ثابت برای محاسبه تبدیل تومان به ریال
#[Route('/api/plugin/check/{plugin}/{bid}', name: 'api_plugin_check')]
public function api_plugin_check($plugin, $bid,Access $access, PluginService $pluginService, EntityManagerInterface $entityManager): Response
{
$acc = $access->hasRole('join');
if (!$acc) {
return $this->json(['active' => false]);
}
$business = $entityManager->getRepository(Business::class)->find($bid);
if (!$business) {
return $this->json(['active' => false]);
}
$isActive = $pluginService->isActive($plugin, $business);
return $this->json(['active' => $isActive]);
}
/**
* بررسی دسترسی کاربر با نقش مشخص
*
@ -266,7 +285,6 @@ class PluginController extends AbstractController
foreach ($plugins as $plugin) {
$plugin->setDateExpire($jdate->jdate('Y/n/d', $plugin->getDateExpire()));
$plugin->setDateSubmit($jdate->jdate('Y/n/d', $plugin->getDateSubmit()));
$plugin->setPrice(number_format($plugin->getPrice()));
}
return $this->json($plugins);
@ -497,6 +515,15 @@ class PluginController extends AbstractController
'icon' => 'import-workflow.png',
'defaultOn' => null,
],
[
'name' => ' مدیریت منابع انسانی',
'code' => 'hrm',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '200000',
'icon' => 'hmr.jpg',
'defaultOn' => null,
],
];
$repo = $entityManager->getRepository(PluginProdect::class);

View file

@ -0,0 +1,907 @@
<?php
namespace App\Controller\Plugins\Hrm;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\PlugHrmAttendance;
use App\Entity\PlugHrmAttendanceItem;
use App\Entity\Person;
use App\Entity\PersonType;
use App\Service\Access;
use App\Service\Log;
use App\Service\Jdate;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
class AttendanceController extends AbstractController
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
#[Route('/api/hrm/attendance/list', name: 'hrm_attendance_list', methods: ['POST'])]
public function list(Request $request, Access $access, Jdate $jdate): JsonResponse
{
try {
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$acc = $access->hasRole('plugHrmAttendance');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
// دریافت پارامترهای فیلتر
$page = $params['page'] ?? 1;
$limit = $params['limit'] ?? 20;
$fromDate = $params['fromDate'] ?? null;
$toDate = $params['toDate'] ?? null;
$personId = $params['personId'] ?? null;
// ایجاد کوئری
$qb = $this->entityManager->createQueryBuilder();
$qb->select('a')
->from(PlugHrmAttendance::class, 'a')
->leftJoin('a.person', 'p')
->addSelect('p')
->where('a.business = :bid')
->setParameter('bid', $acc['bid']);
// اعمال فیلترها
if ($fromDate) {
$qb->andWhere('a.date >= :fromDate')
->setParameter('fromDate', $fromDate);
}
if ($toDate) {
$qb->andWhere('a.date <= :toDate')
->setParameter('toDate', $toDate);
}
if ($personId) {
$qb->andWhere('a.person = :personId')
->setParameter('personId', $personId);
}
$qb->orderBy('a.date', 'DESC')
->addOrderBy('p.nikename', 'ASC');
// محاسبه تعداد کل
$countQb = clone $qb;
$totalCount = $countQb->select('COUNT(a.id)')->getQuery()->getSingleScalarResult();
// اعمال صفحه‌بندی
$qb->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit);
$attendances = $qb->getQuery()->getResult();
// تبدیل به آرایه
$result = [];
foreach ($attendances as $attendance) {
$result[] = [
'id' => $attendance->getId(),
'date' => $attendance->getDate(),
'personId' => $attendance->getPerson()->getId(),
'personName' => $attendance->getPerson()->getNikename(),
'totalHours' => $attendance->getTotalHours(),
'overtimeHours' => $attendance->getOvertimeHours(),
'description' => $attendance->getDescription(),
'createdAt' => $jdate->jdate('Y/n/d H:i', $attendance->getCreatedAt()->getTimestamp()),
];
}
return $this->json([
'data' => $result,
'total' => $totalCount,
'page' => $page,
'limit' => $limit,
'pages' => ceil($totalCount / $limit)
]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 500);
}
}
#[Route('/api/hrm/attendance/mod/{id}', name: 'hrm_attendance_mod', methods: ['POST'])]
public function mod(Request $request, Access $access, Log $log, Jdate $jdate, $id = 0): JsonResponse
{
try {
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$acc = $access->hasRole('plugHrmAttendance');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
if ($id == 0) {
// ایجاد تردد جدید
$attendance = new PlugHrmAttendance();
$attendance->setBusiness($acc['bid']);
} else {
// ویرایش تردد موجود
$attendance = $this->entityManager->getRepository(PlugHrmAttendance::class)->find($id);
if (!$attendance || $attendance->getBusiness()->getId() != $acc['bid']->getId()) {
throw $this->createNotFoundException('تردد یافت نشد.');
}
}
// بررسی وجود پرسنل
$person = $this->entityManager->getRepository(Person::class)->find($params['personId']);
if (!$person || $person->getBid()->getId() != $acc['bid']->getId()) {
throw $this->createNotFoundException('پرسنل یافت نشد.');
}
$attendance->setPerson($person);
$attendance->setDate($params['date']);
$attendance->setDescription($params['description'] ?? '');
$attendance->setUpdatedAt(new \DateTime());
// محاسبه ساعات کار
$totalHours = 0;
$overtimeHours = 0;
if (isset($params['items']) && is_array($params['items'])) {
// حذف آیتم‌های قبلی
foreach ($attendance->getItems() as $item) {
$this->entityManager->remove($item);
}
// افزودن آیتم‌های جدید
foreach ($params['items'] as $itemData) {
$item = new PlugHrmAttendanceItem();
$item->setAttendance($attendance);
$item->setType($itemData['type']); // ورود یا خروج
$item->setTime($itemData['time']); // HH:MM
$item->setTimestamp($itemData['timestamp']);
$this->entityManager->persist($item);
}
// محاسبه ساعات کار
$workHours = $this->calculateWorkHours($params['items']);
$totalHours = $workHours['total'];
$overtimeHours = $workHours['overtime'];
}
$attendance->setTotalHours($totalHours);
$attendance->setOvertimeHours($overtimeHours);
$this->entityManager->persist($attendance);
$this->entityManager->flush();
$log->insert(
'تردد پرسنل',
'تردد پرسنل ' . $person->getNikename() . ' برای تاریخ ' . $params['date'] . ' ثبت شد.',
$this->getUser(),
$acc['bid']
);
return $this->json(['result' => 1, 'id' => $attendance->getId()]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 500);
}
}
#[Route('/api/hrm/attendance/get/{id}', name: 'hrm_attendance_get', methods: ['POST'])]
public function get(Request $request, Access $access, Jdate $jdate, $id): JsonResponse
{
try {
$acc = $access->hasRole('plugHrmAttendance');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$attendance = $this->entityManager->getRepository(PlugHrmAttendance::class)->find($id);
if (!$attendance || $attendance->getBusiness()->getId() != $acc['bid']->getId()) {
throw $this->createNotFoundException('تردد یافت نشد.');
}
$items = [];
foreach ($attendance->getItems() as $item) {
$items[] = [
'id' => $item->getId(),
'type' => $item->getType(),
'time' => $item->getTime(),
'timestamp' => $item->getTimestamp(),
];
}
$result = [
'id' => $attendance->getId(),
'personId' => $attendance->getPerson()->getId(),
'personName' => $attendance->getPerson()->getNikename(),
'date' => $attendance->getDate(),
'totalHours' => $attendance->getTotalHours(),
'overtimeHours' => $attendance->getOvertimeHours(),
'description' => $attendance->getDescription(),
'items' => $items,
];
return $this->json($result);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 500);
}
}
#[Route('/api/hrm/attendance/delete/{id}', name: 'hrm_attendance_delete', methods: ['POST'])]
public function delete(Request $request, Access $access, Log $log, $id): JsonResponse
{
try {
$acc = $access->hasRole('plugHrmAttendance');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$attendance = $this->entityManager->getRepository(PlugHrmAttendance::class)->find($id);
if (!$attendance || $attendance->getBusiness()->getId() != $acc['bid']->getId()) {
throw $this->createNotFoundException('تردد یافت نشد.');
}
$personName = $attendance->getPerson()->getNikename();
$date = $attendance->getDate();
$this->entityManager->remove($attendance);
$this->entityManager->flush();
$log->insert(
'تردد پرسنل',
'تردد پرسنل ' . $personName . ' برای تاریخ ' . $date . ' حذف شد.',
$this->getUser(),
$acc['bid']
);
return $this->json(['result' => 1]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 500);
}
}
#[Route('/api/hrm/attendance/employees', name: 'hrm_attendance_employees', methods: ['POST'])]
public function getEmployees(Request $request, Access $access): JsonResponse
{
try {
$acc = $access->hasRole('plugHrmAttendance');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$search = $params['search'] ?? '';
$page = $params['page'] ?? 1;
$limit = $params['limit'] ?? 20;
// دریافت نوع پرسنل "کارمند" - کد صحیح در دیتابیس: emplyee
$employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'emplyee']);
if (!$employeeType) {
// اگر نوع "emplyee" وجود نداشت، نوع "کارمند" را جستجو کن
$employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'کارمند']);
}
$qb = $this->entityManager->createQueryBuilder();
$qb->select('p')
->from(Person::class, 'p')
->where('p.bid = :bid')
->setParameter('bid', $acc['bid']);
// فقط پرسنل‌هایی که نوع پرسنل دارند
if ($employeeType) {
$qb->join('p.type', 't')
->andWhere('t = :employeeType')
->setParameter('employeeType', $employeeType);
} else {
// اگر نوع کارمند پیدا نشد، حداقل پرسنل‌هایی که نوع دارند را برگردان
$qb->join('p.type', 't');
}
// اعمال فیلتر جستجو
if (!empty($search)) {
$qb->andWhere('p.nikename LIKE :search OR p.name LIKE :search OR p.code LIKE :search')
->setParameter('search', '%' . $search . '%');
}
$qb->orderBy('p.nikename', 'ASC');
// محاسبه تعداد کل
$countQb = clone $qb;
$totalCount = $countQb->select('COUNT(p.id)')->getQuery()->getSingleScalarResult();
// اعمال صفحه‌بندی
$qb->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit);
$employees = $qb->getQuery()->getResult();
$result = [];
foreach ($employees as $employee) {
$result[] = [
'id' => $employee->getId(),
'name' => $employee->getNikename(),
'code' => $employee->getCode(),
'mobile' => $employee->getMobile(),
'tel' => $employee->getTel(),
'email' => $employee->getEmail(),
'company' => $employee->getCompany(),
'address' => $employee->getAddress(),
];
}
return $this->json([
'items' => $result,
'total' => $totalCount,
'page' => $page,
'limit' => $limit
]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 500);
}
}
#[Route('/api/hrm/attendance/employees/search', name: 'hrm_attendance_employees_search', methods: ['POST'])]
public function searchEmployees(Request $request, Access $access): JsonResponse
{
try {
$acc = $access->hasRole('plugHrmAttendance');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$search = $params['search'] ?? '';
$page = $params['page'] ?? 1;
$limit = $params['limit'] ?? 10;
// فیلترهای پیشرفته
$code = $params['code'] ?? '';
$mobile = $params['mobile'] ?? '';
$company = $params['company'] ?? '';
$email = $params['email'] ?? '';
// دریافت نوع پرسنل "کارمند" - کد صحیح در دیتابیس: emplyee
$employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'emplyee']);
if (!$employeeType) {
$employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'کارمند']);
}
$qb = $this->entityManager->createQueryBuilder();
$qb->select('p')
->from(Person::class, 'p')
->where('p.bid = :bid')
->setParameter('bid', $acc['bid']);
// فقط پرسنل‌هایی که نوع پرسنل دارند
if ($employeeType) {
$qb->join('p.type', 't')
->andWhere('t = :employeeType')
->setParameter('employeeType', $employeeType);
} else {
// اگر نوع کارمند پیدا نشد، حداقل پرسنل‌هایی که نوع دارند را برگردان
$qb->join('p.type', 't');
}
// اعمال فیلتر جستجو عمومی
if (!empty($search)) {
$qb->andWhere('p.nikename LIKE :search OR p.name LIKE :search OR p.code LIKE :search OR p.mobile LIKE :search')
->setParameter('search', '%' . $search . '%');
}
// اعمال فیلترهای پیشرفته
if (!empty($code)) {
$qb->andWhere('p.code LIKE :code')
->setParameter('code', '%' . $code . '%');
}
if (!empty($mobile)) {
$qb->andWhere('p.mobile LIKE :mobile')
->setParameter('mobile', '%' . $mobile . '%');
}
if (!empty($company)) {
$qb->andWhere('p.company LIKE :company')
->setParameter('company', '%' . $company . '%');
}
if (!empty($email)) {
$qb->andWhere('p.email LIKE :email')
->setParameter('email', '%' . $email . '%');
}
$qb->orderBy('p.nikename', 'ASC');
// محاسبه تعداد کل
$countQb = clone $qb;
$totalCount = $countQb->select('COUNT(p.id)')->getQuery()->getSingleScalarResult();
// اعمال صفحه‌بندی
$qb->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit);
$employees = $qb->getQuery()->getResult();
$result = [];
foreach ($employees as $employee) {
$result[] = [
'id' => $employee->getId(),
'name' => $employee->getNikename(),
'code' => $employee->getCode(),
'mobile' => $employee->getMobile(),
'tel' => $employee->getTel(),
'email' => $employee->getEmail(),
'company' => $employee->getCompany(),
'address' => $employee->getAddress(),
'fullName' => $employee->getName(),
];
}
return $this->json([
'items' => $result,
'total' => $totalCount,
'page' => $page,
'limit' => $limit
]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 500);
}
}
#[Route('/api/hrm/attendance/employees/test', name: 'hrm_attendance_employees_test', methods: ['GET'])]
public function testEmployees(Access $access): JsonResponse
{
try {
$acc = $access->hasRole('plugHrmAttendance');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
// دریافت نوع پرسنل "کارمند"
$employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'emplyee']);
$qb = $this->entityManager->createQueryBuilder();
$qb->select('p')
->from(Person::class, 'p')
->where('p.bid = :bid')
->setParameter('bid', $acc['bid']);
if ($employeeType) {
$qb->join('p.type', 't')
->andWhere('t = :employeeType')
->setParameter('employeeType', $employeeType);
} else {
$qb->join('p.type', 't');
}
$qb->setMaxResults(5);
$employees = $qb->getQuery()->getResult();
$result = [];
foreach ($employees as $employee) {
$result[] = [
'id' => $employee->getId(),
'name' => $employee->getNikename(),
'code' => $employee->getCode(),
'bid' => $employee->getBid()->getId(),
'hasType' => $employee->getType()->count() > 0
];
}
return $this->json([
'business_id' => $acc['bid'],
'employee_type_found' => $employeeType ? true : false,
'employee_type_code' => $employeeType ? $employeeType->getCode() : null,
'total_employees' => count($result),
'employees' => $result
]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 500);
}
}
#[Route('/api/hrm/attendance/import', name: 'hrm_attendance_import', methods: ['POST'])]
public function import(Request $request, Access $access, Log $log, Jdate $jdate): JsonResponse
{
try {
$acc = $access->hasRole('plugHrmAttendance');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$file = $request->files->get('file');
if (!$file) {
throw new \Exception('فایل انتخاب نشده است.');
}
$spreadsheet = IOFactory::load($file->getPathname());
$worksheet = $spreadsheet->getActiveSheet();
$rows = $worksheet->toArray();
// حذف سطر هدر
array_shift($rows);
$importedCount = 0;
$errors = [];
foreach ($rows as $index => $row) {
try {
if (empty($row[0]) || empty($row[1]) || empty($row[2])) {
continue; // رد کردن سطرهای خالی
}
$personCode = $row[0];
$date = $row[1];
$time = $row[2];
$type = $row[3] ?? 'ورود'; // ورود یا خروج
// پیدا کردن پرسنل بر اساس کد
$person = $this->entityManager->getRepository(Person::class)->findOneBy([
'code' => $personCode,
'bid' => $acc['bid']
]);
if (!$person) {
$errors[] = "سطر " . ($index + 2) . ": پرسنل با کد $personCode یافت نشد.";
continue;
}
// بررسی وجود تردد برای این تاریخ
$attendance = $this->entityManager->getRepository(PlugHrmAttendance::class)
->findByBusinessAndPersonAndDate($acc['bid'], $person, $date);
if (!$attendance) {
$attendance = new PlugHrmAttendance();
$attendance->setBusiness($acc['bid']);
$attendance->setPerson($person);
$attendance->setDate($date);
$attendance->setTotalHours(0);
$attendance->setOvertimeHours(0);
}
// افزودن آیتم تردد
$item = new PlugHrmAttendanceItem();
$item->setAttendance($attendance);
$item->setType($type);
$item->setTime($time);
$item->setTimestamp($jdate->jallaliToUnixTime($date . ' ' . $time));
$this->entityManager->persist($item);
$this->entityManager->persist($attendance);
$importedCount++;
} catch (\Exception $e) {
$errors[] = "سطر " . ($index + 2) . ": " . $e->getMessage();
}
}
$this->entityManager->flush();
$log->insert(
'تردد پرسنل',
"$importedCount رکورد تردد از فایل اکسل وارد شد.",
$this->getUser(),
$acc['bid']
);
return $this->json([
'result' => 1,
'importedCount' => $importedCount,
'errors' => $errors
]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 500);
}
}
#[Route('/api/hrm/attendance/export', name: 'hrm_attendance_export', methods: ['POST'])]
public function export(Request $request, Access $access, Jdate $jdate): JsonResponse
{
try {
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$acc = $access->hasRole('plugHrmAttendance');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$fromDate = $params['fromDate'] ?? null;
$toDate = $params['toDate'] ?? null;
$personId = $params['personId'] ?? null;
$attendances = $this->entityManager->getRepository(PlugHrmAttendance::class)
->findByBusinessAndDateRange($acc['bid'], $fromDate, $toDate, $personId);
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// تنظیم هدر
$sheet->setCellValue('A1', 'کد پرسنل');
$sheet->setCellValue('B1', 'نام پرسنل');
$sheet->setCellValue('C1', 'تاریخ');
$sheet->setCellValue('D1', 'ساعات کل کار');
$sheet->setCellValue('E1', 'ساعات اضافه‌کاری');
$sheet->setCellValue('F1', 'توضیحات');
$row = 2;
foreach ($attendances as $attendance) {
$sheet->setCellValue('A' . $row, $attendance->getPerson()->getCode());
$sheet->setCellValue('B' . $row, $attendance->getPerson()->getNikename());
$sheet->setCellValue('C' . $row, $attendance->getDate());
$sheet->setCellValue('D' . $row, $this->formatMinutesToHours($attendance->getTotalHours()));
$sheet->setCellValue('E' . $row, $this->formatMinutesToHours($attendance->getOvertimeHours()));
$sheet->setCellValue('F' . $row, $attendance->getDescription());
$row++;
}
$writer = new Xlsx($spreadsheet);
$filename = 'attendance_export_' . date('Y-m-d_H-i-s') . '.xlsx';
$filepath = sys_get_temp_dir() . '/' . $filename;
$writer->save($filepath);
return $this->json([
'result' => 1,
'filename' => $filename,
'downloadUrl' => '/api/hrm/attendance/download/' . $filename
]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 500);
}
}
#[Route('/api/hrm/attendance/download/{filename}', name: 'hrm_attendance_download', methods: ['GET'])]
public function download(Request $request, Access $access, $filename): JsonResponse
{
try {
$acc = $access->hasRole('plugHrmAttendance');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$filepath = sys_get_temp_dir() . '/' . $filename;
if (!file_exists($filepath)) {
throw $this->createNotFoundException('فایل یافت نشد.');
}
$response = new JsonResponse();
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"');
$response->setContent(file_get_contents($filepath));
unlink($filepath); // حذف فایل موقت
return $response;
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 500);
}
}
#[Route('/api/hrm/attendance/reports', name: 'hrm_attendance_reports', methods: ['POST'])]
public function reports(Request $request, Access $access, Jdate $jdate): JsonResponse
{
try {
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$acc = $access->hasRole('plugHrmAttendance');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$type = $params['type'] ?? 'daily';
$fromDate = $params['fromDate'] ?? null;
$toDate = $params['toDate'] ?? null;
$personId = $params['personId'] ?? null;
$attendances = $this->entityManager->getRepository(PlugHrmAttendance::class)
->findByBusinessAndDateRange($acc['bid'], $fromDate, $toDate, $personId);
$reportData = [];
$summary = [
'totalDays' => 0,
'totalHours' => 0,
'totalOvertime' => 0,
'averageHours' => 0
];
foreach ($attendances as $attendance) {
$reportData[] = [
'id' => $attendance->getId(),
'date' => $attendance->getDate(),
'personName' => $attendance->getPerson()->getNikename(),
'personCode' => $attendance->getPerson()->getCode(),
'totalHours' => $attendance->getTotalHours(),
'overtimeHours' => $attendance->getOvertimeHours(),
'description' => $attendance->getDescription(),
'status' => $this->getAttendanceStatus($attendance)
];
$summary['totalDays']++;
$summary['totalHours'] += $attendance->getTotalHours();
$summary['totalOvertime'] += $attendance->getOvertimeHours();
}
if ($summary['totalDays'] > 0) {
$summary['averageHours'] = round($summary['totalHours'] / $summary['totalDays']);
}
return $this->json([
'result' => 1,
'data' => $reportData,
'summary' => $summary
]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 500);
}
}
#[Route('/api/hrm/attendance/export-report', name: 'hrm_attendance_export_report', methods: ['POST'])]
public function exportReport(Request $request, Access $access, Jdate $jdate): JsonResponse
{
try {
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$acc = $access->hasRole('plugHrmAttendance');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$type = $params['type'] ?? 'daily';
$fromDate = $params['fromDate'] ?? null;
$toDate = $params['toDate'] ?? null;
$personId = $params['personId'] ?? null;
$attendances = $this->entityManager->getRepository(PlugHrmAttendance::class)
->findByBusinessAndDateRange($acc['bid'], $fromDate, $toDate, $personId);
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// تنظیم هدر بر اساس نوع گزارش
$headers = ['تاریخ', 'نام پرسنل', 'کد پرسنل', 'ساعات کل کار', 'ساعات اضافه‌کاری'];
if ($type === 'absence') {
$headers[] = 'وضعیت';
}
$headers[] = 'توضیحات';
foreach ($headers as $index => $header) {
$sheet->setCellValue(chr(65 + $index) . '1', $header);
}
$row = 2;
foreach ($attendances as $attendance) {
$sheet->setCellValue('A' . $row, $attendance->getDate());
$sheet->setCellValue('B' . $row, $attendance->getPerson()->getNikename());
$sheet->setCellValue('C' . $row, $attendance->getPerson()->getCode());
$sheet->setCellValue('D' . $row, $this->formatMinutesToHours($attendance->getTotalHours()));
$sheet->setCellValue('E' . $row, $this->formatMinutesToHours($attendance->getOvertimeHours()));
if ($type === 'absence') {
$sheet->setCellValue('F' . $row, $this->getAttendanceStatus($attendance));
$sheet->setCellValue('G' . $row, $attendance->getDescription());
} else {
$sheet->setCellValue('F' . $row, $attendance->getDescription());
}
$row++;
}
$writer = new Xlsx($spreadsheet);
$filename = 'attendance_report_' . $type . '_' . date('Y-m-d_H-i-s') . '.xlsx';
$filepath = sys_get_temp_dir() . '/' . $filename;
$writer->save($filepath);
return $this->json([
'result' => 1,
'filename' => $filename,
'downloadUrl' => '/api/hrm/attendance/download/' . $filename
]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 500);
}
}
private function getAttendanceStatus(PlugHrmAttendance $attendance): string
{
$totalHours = $attendance->getTotalHours();
$overtimeHours = $attendance->getOvertimeHours();
// استاندارد 8 ساعت کار در روز
$standardHours = 8 * 60; // به دقیقه
if ($totalHours >= $standardHours) {
return 'حضور کامل';
} elseif ($totalHours >= $standardHours * 0.5) {
return 'نیمه وقت';
} elseif ($totalHours > 0) {
return 'تاخیر';
} else {
return 'غیبت';
}
}
private function calculateWorkHours($items): array
{
$totalMinutes = 0;
$overtimeMinutes = 0;
$standardWorkMinutes = 8 * 60; // 8 ساعت کار استاندارد
// مرتب کردن آیتم‌ها بر اساس زمان
usort($items, function($a, $b) {
return strtotime($a['time']) - strtotime($b['time']);
});
$entries = [];
$exits = [];
foreach ($items as $item) {
if ($item['type'] === 'ورود') {
$entries[] = $item['time'];
} elseif ($item['type'] === 'خروج') {
$exits[] = $item['time'];
}
}
// محاسبه ساعات کار
$minCount = min(count($entries), count($exits));
for ($i = 0; $i < $minCount; $i++) {
$entryTime = strtotime($entries[$i]);
$exitTime = strtotime($exits[$i]);
$workMinutes = ($exitTime - $entryTime) / 60;
$totalMinutes += $workMinutes;
}
if ($totalMinutes > $standardWorkMinutes) {
$overtimeMinutes = $totalMinutes - $standardWorkMinutes;
$totalMinutes = $standardWorkMinutes;
}
return [
'total' => $totalMinutes,
'overtime' => $overtimeMinutes
];
}
private function formatMinutesToHours($minutes): string
{
if (!$minutes) return '0:00';
$hours = floor($minutes / 60);
$mins = $minutes % 60;
return sprintf('%d:%02d', $hours, $mins);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,10 @@
<?php
/**
* Developed by Mohammad Rezai
* https://pirouz.xyz 2025-08-24
*/
namespace App\Controller\Plugins;
use App\Repository\PlugWarrantySerialRepository;
@ -15,14 +20,13 @@ use App\Entity\HesabdariDoc;
use App\Entity\Business;
use App\Service\Access;
use App\Service\Log;
use App\Service\Provider;
use App\Service\Jdate;
use PhpOffice\PhpSpreadsheet\IOFactory;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use App\Service\PluginService;
use App\Service\SMS;
use App\Service\registryMGR;
use Symfony\Component\Validator\Constraints as Assert;
use App\Entity\StoreroomTicket;
use OpenApi\Annotations as OA;
class PlugWarrantyController extends AbstractController
{
@ -39,6 +43,108 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* دریافت سریال‌های گارانتی بر اساس حواله انبار
*
* @OA\Get(
* path="/api/plugins/warranty/serials/by-storeroom-ticket/{code}",
* summary="دریافت سریال‌های گارانتی مرتبط با حواله انبار",
* tags={"Warranty Serials"},
* @OA\Parameter(
* name="code",
* in="path",
* description="کد حواله انبار",
* required=true,
* @OA\Schema(type="string")
* ),
* @OA\Response(
* response=200,
* description="سریال‌های گارانتی حواله",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="ticketActivationCode", type="string", description="کد فعال‌سازی حواله"),
* @OA\Property(property="items", type="array", @OA\Items(
* type="object",
* @OA\Property(property="serialNumber", type="string", description="شماره سریال"),
* @OA\Property(property="commoditySerial", type="string", description="سریال کالا"),
* @OA\Property(property="commodity", type="object",
* @OA\Property(property="id", type="integer", description="شناسه کالا"),
* @OA\Property(property="name", type="string", description="نام کالا"),
* @OA\Property(property="code", type="string", description="کد کالا")
* ),
* @OA\Property(property="status", type="string", description="وضعیت"),
* @OA\Property(property="activation", type="string", description="فعال‌سازی"),
* @OA\Property(property="activationTicketCode", type="string", description="کد حواله فعال‌سازی"),
* @OA\Property(property="warrantyEndDate", type="string", description="تاریخ پایان گارانتی")
* ))
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=404, description="حواله یافت نشد")
* )
*/
#[Route('/api/plugins/warranty/serials/by-storeroom-ticket/{code}', name: 'plugin_warranty_serials_by_storeroom_ticket', methods: ['GET'])]
public function plugin_warranty_serials_by_storeroom_ticket(string $code, Request $request, Access $access, EntityManagerInterface $entityManager, PluginService $pluginService): JsonResponse
{
$acc = $access->hasRole('store');
if (!$acc) {
throw $this->createAccessDeniedException();
}
if (!$pluginService->isActive('warranty', $acc['bid'])) {
return $this->json(['success' => false, 'message' => 'افزونه گارانتی فعال نیست'], 403);
}
/** @var StoreroomTicket|null $ticket */
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
]);
if (!$ticket) {
return $this->json(['success' => false, 'message' => 'حواله یافت نشد'], 404);
}
$doc = $ticket->getDoc();
if (!$doc) {
return $this->json([
'success' => true,
'ticketActivationCode' => $ticket->getActivationCode(),
'items' => []
]);
}
$serials = $entityManager->getRepository(PlugWarrantySerial::class)->createQueryBuilder('s')
->andWhere('s.business = :bid')
->andWhere('s.activationTicketCode = :code')
->setParameter('bid', $acc['bid'])
->setParameter('code', $code)
->getQuery()
->getResult();
$items = array_map(function (PlugWarrantySerial $s) use ($entityManager) {
$commodity = $s->getCommodity();
return [
'serialNumber' => $s->getSerialNumber(),
'commoditySerial' => $s->getCommoditySerial(),
'commodity' => $commodity ? [
'id' => $commodity->getId(),
'name' => $commodity->getName(),
'code' => $commodity->getCode(),
] : null,
'status' => $s->getStatus(),
'activation' => $s->getActivation(),
'activationTicketCode' => $s->getActivationTicketCode(),
'warrantyEndDate' => $s->getWarrantyEndDate()?->format('Y-m-d'),
];
}, $serials);
return $this->json([
'success' => true,
'ticketActivationCode' => $ticket->getActivationCode(),
'items' => $items
]);
}
private function expiredFlag(?\DateTimeImmutable $end): bool
{
return $end !== null && $end < new \DateTimeImmutable('today');
@ -49,6 +155,45 @@ class PlugWarrantyController extends AbstractController
$this->entityManager = $entityManager;
}
/**
* درخواست تخصیص سریال‌های گارانتی
*
* @OA\Post(
* path="/api/plugins/warranty/assign/request",
* summary="درخواست تخصیص سریال‌های گارانتی به اسناد",
* tags={"Warranty Assignment"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"items"},
* @OA\Property(property="items", type="array", @OA\Items(
* type="object",
* @OA\Property(property="commodity_id", type="integer", description="شناسه کالا"),
* @OA\Property(property="count", type="integer", description="تعداد"),
* @OA\Property(property="document_id", type="integer", description="شناسه سند"),
* @OA\Property(property="document_item_id", type="integer", description="شناسه آیتم سند")
* ), description="لیست اقلام برای تخصیص")
* )
* ),
* @OA\Response(
* response=200,
* description="سریال‌ها با موفقیت تخصیص داده شدند",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="allocated", type="array", @OA\Items(
* type="object",
* @OA\Property(property="commodity_id", type="integer", description="شناسه کالا"),
* @OA\Property(property="allocated", type="array", @OA\Items(type="integer"), description="شناسه‌های سریال‌های تخصیص داده شده")
* ))
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="پارامترهای نامعتبر"),
* @OA\Response(response=404, description="کسب‌وکار یافت نشد"),
* @OA\Response(response=409, description="کد گارانتی کافی موجود نیست")
* )
*/
#[Route('/api/plugins/warranty/assign/request', name: 'plugin_warranty_assign_request', methods: ['POST'])]
public function plugin_warranty_assign_request(Request $request, EntityManagerInterface $em, Access $access, PluginService $pluginService): JsonResponse
{
@ -117,6 +262,40 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* اسکن و تأیید سریال گارانتی
*
* @OA\Post(
* path="/api/plugins/warranty/assign/scan",
* summary="اسکن و تأیید سریال گارانتی برای تخصیص",
* tags={"Warranty Assignment"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"serialNumber", "commodity_id", "document_item_id"},
* @OA\Property(property="serialNumber", type="string", description="شماره سریال"),
* @OA\Property(property="commodity_id", type="integer", description="شناسه کالا"),
* @OA\Property(property="document_id", type="integer", description="شناسه سند"),
* @OA\Property(property="document_item_id", type="integer", description="شناسه آیتم سند"),
* @OA\Property(property="physicalBoxBarcode", type="string", description="بارکد فیزیکی جعبه"),
* @OA\Property(property="productTypeCode", type="string", description="کد نوع محصول")
* )
* ),
* @OA\Response(
* response=200,
* description="سریال با موفقیت تأیید شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="تأیید شد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="پارامترهای ناقص"),
* @OA\Response(response=404, description="کد گارانتی یافت نشد"),
* @OA\Response(response=409, description="وضعیت کد برای اسکن معتبر نیست")
* )
*/
#[Route('/api/plugins/warranty/assign/scan', name: 'plugin_warranty_assign_scan', methods: ['POST'])]
public function plugin_warranty_assign_scan(Request $request, EntityManagerInterface $entityManager, Access $access, PluginService $pluginService): JsonResponse
{
@ -178,6 +357,40 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* دریافت سریال‌های گارانتی بر اساس فاکتور
*
* @OA\Get(
* path="/api/plugins/warranty/serials/by-invoice/{code}",
* summary="دریافت سریال‌های گارانتی مرتبط با فاکتور",
* tags={"Warranty Serials"},
* @OA\Parameter(
* name="code",
* in="path",
* description="کد فاکتور",
* required=true,
* @OA\Schema(type="string")
* ),
* @OA\Response(
* response=200,
* description="سریال‌های گارانتی فاکتور",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="items", type="array", @OA\Items(
* type="object",
* @OA\Property(property="serialNumber", type="string", description="شماره سریال"),
* @OA\Property(property="commodity", type="string", description="نام کالا"),
* @OA\Property(property="status", type="string", description="وضعیت"),
* @OA\Property(property="warrantyEndDate", type="string", description="تاریخ پایان گارانتی"),
* @OA\Property(property="expired", type="boolean", description="آیا منقضی شده")
* ))
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=404, description="فاکتور یافت نشد")
* )
*/
#[Route('/api/plugins/warranty/serials/by-invoice/{code}', name: 'plugin_warranty_serials_by_invoice', methods: ['GET'])]
public function plugin_warranty_serials_by_invoice(string $code, Request $request, Access $access, EntityManagerInterface $entityManager, PluginService $pluginService): JsonResponse
{
@ -221,6 +434,34 @@ class PlugWarrantyController extends AbstractController
return $this->json(['success' => true, 'items' => $result]);
}
/**
* ارسال سریال‌های گارانتی
*
* @OA\Post(
* path="/api/plugins/warranty/send-serials",
* summary="ارسال سریال‌های گارانتی فاکتور به مشتری",
* tags={"Warranty Serials"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"invoiceCode", "mobile"},
* @OA\Property(property="invoiceCode", type="string", description="کد فاکتور"),
* @OA\Property(property="mobile", type="string", description="شماره موبایل مشتری")
* )
* ),
* @OA\Response(
* response=200,
* description="سریال‌ها با موفقیت ارسال شدند",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true)
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="پارامترهای ناقص"),
* @OA\Response(response=404, description="فاکتور یافت نشد")
* )
*/
#[Route('/api/plugins/warranty/send-serials', name: 'plugin_warranty_send_serials', methods: ['POST'])]
public function plugin_warranty_send_serials(Request $request, Access $access, EntityManagerInterface $entityManager, PluginService $pluginService, SMS $SMS, registryMGR $registryMGR): JsonResponse
{
@ -270,6 +511,35 @@ class PlugWarrantyController extends AbstractController
return $this->json(['success' => true]);
}
/**
* پیش‌نمایش وارد کردن سریال‌های گارانتی
*
* @OA\Post(
* path="/api/plugins/warranty/serials/preview-import",
* summary="پیش‌نمایش فایل اکسل برای وارد کردن سریال‌های گارانتی",
* tags={"Warranty Import"},
* @OA\RequestBody(
* required=true,
* @OA\MediaType(
* mediaType="multipart/form-data",
* @OA\Schema(
* @OA\Property(property="file", type="string", format="binary", description="فایل اکسل")
* )
* )
* ),
* @OA\Response(
* response=200,
* description="پیش‌نمایش داده‌های فایل",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="preview", type="array", @OA\Items(type="object"), description="داده‌های پیش‌نمایش"),
* @OA\Property(property="errors", type="array", @OA\Items(type="string"), description="خطاهای موجود")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="فایل ارسال نشده است")
* )
*/
#[Route('/api/plugins/warranty/serials/preview-import', name: 'plugin_warranty_serial_preview_import', methods: ['POST'])]
public function plugin_warranty_serial_preview_import(Request $request, Access $access): JsonResponse
{
@ -336,6 +606,34 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* وارد کردن سریال‌های گارانتی از اکسل
*
* @OA\Post(
* path="/api/plugins/warranty/serials/import-excel",
* summary="وارد کردن سریال‌های گارانتی از فایل اکسل",
* tags={"Warranty Import"},
* @OA\RequestBody(
* required=true,
* @OA\MediaType(
* mediaType="multipart/form-data",
* @OA\Schema(
* @OA\Property(property="file", type="string", format="binary", description="فایل اکسل")
* )
* )
* ),
* @OA\Response(
* response=200,
* description="داده‌های استخراج شده از فایل",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="serials", type="array", @OA\Items(type="object"), description="سریال‌های استخراج شده")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="فایل ارسال نشده است")
* )
*/
#[Route('/api/plugins/warranty/serials/import-excel', name: 'plugin_warranty_serial_import_excel', methods: ['POST'])]
public function plugin_warranty_serial_import_excel(Request $request, EntityManagerInterface $entityManager, Access $access): JsonResponse
{
@ -396,6 +694,60 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* دریافت لیست سریال‌های گارانتی
*
* @OA\Get(
* path="/api/plugins/warranty/serials",
* summary="دریافت لیست سریال‌های گارانتی با فیلترهای مختلف",
* tags={"Warranty Serials"},
* @OA\Parameter(name="page", in="query", description="شماره صفحه", @OA\Schema(type="integer", default=1)),
* @OA\Parameter(name="limit", in="query", description="تعداد در هر صفحه", @OA\Schema(type="integer", default=20)),
* @OA\Parameter(name="status", in="query", description="فیلتر بر اساس وضعیت", @OA\Schema(type="string")),
* @OA\Parameter(name="commodity_id", in="query", description="فیلتر بر اساس کالا", @OA\Schema(type="integer")),
* @OA\Parameter(name="search", in="query", description="جستجو در سریال‌ها", @OA\Schema(type="string")),
* @OA\Response(
* response=200,
* description="لیست سریال‌های گارانتی",
* @OA\JsonContent(
* type="array",
* @OA\Items(
* type="object",
* @OA\Property(property="id", type="integer", description="شناسه سریال"),
* @OA\Property(property="serialNumber", type="string", description="شماره سریال"),
* @OA\Property(property="commodity", type="object",
* @OA\Property(property="id", type="integer", description="شناسه کالا"),
* @OA\Property(property="name", type="string", description="نام کالا"),
* @OA\Property(property="code", type="string", description="کد کالا")
* ),
* @OA\Property(property="dateSubmit", type="string", description="تاریخ ثبت"),
* @OA\Property(property="description", type="string", description="توضیحات"),
* @OA\Property(property="warrantyStartDate", type="string", description="تاریخ شروع گارانتی"),
* @OA\Property(property="warrantyEndDate", type="string", description="تاریخ پایان گارانتی"),
* @OA\Property(property="status", type="string", description="وضعیت"),
* @OA\Property(property="activation", type="string", description="فعال‌سازی"),
* @OA\Property(property="notes", type="string", description="یادداشت‌ها"),
* @OA\Property(property="expired", type="boolean", description="آیا منقضی شده"),
* @OA\Property(property="submitter", type="object",
* @OA\Property(property="id", type="integer", description="شناسه ثبت‌کننده"),
* @OA\Property(property="name", type="string", description="نام ثبت‌کننده")
* ),
* @OA\Property(property="commoditySerial", type="string", description="سریال کالا"),
* @OA\Property(property="buyer", type="object",
* @OA\Property(property="id", type="integer", description="شناسه خریدار"),
* @OA\Property(property="code", type="string", description="کد خریدار"),
* @OA\Property(property="name", type="string", description="نام خریدار"),
* @OA\Property(property="nikename", type="string", description="نام مستعار"),
* @OA\Property(property="mobile", type="string", description="موبایل")
* ),
* @OA\Property(property="activationAt", type="string", description="تاریخ فعال‌سازی"),
* @OA\Property(property="allocatedToDocumentCode", type="string", description="کد سند تخصیص داده شده")
* )
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز")
* )
*/
#[Route('/api/plugins/warranty/serials', name: 'plugin_warranty_serials', methods: ['GET'])]
public function plugin_warranty_serials(EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, Request $request): JsonResponse
{
@ -469,6 +821,60 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* دریافت جزئیات یک سریال گارانتی
*
* @OA\Get(
* path="/api/plugins/warranty/serials/{id}",
* summary="دریافت جزئیات کامل یک سریال گارانتی",
* tags={"Warranty Serials"},
* @OA\Parameter(
* name="id",
* in="path",
* description="شناسه سریال گارانتی",
* required=true,
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="جزئیات سریال گارانتی",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="id", type="integer", description="شناسه سریال"),
* @OA\Property(property="serialNumber", type="string", description="شماره سریال"),
* @OA\Property(property="commodity", type="object",
* @OA\Property(property="id", type="integer", description="شناسه کالا"),
* @OA\Property(property="name", type="string", description="نام کالا"),
* @OA\Property(property="code", type="string", description="کد کالا")
* ),
* @OA\Property(property="dateSubmit", type="string", description="تاریخ ثبت"),
* @OA\Property(property="description", type="string", description="توضیحات"),
* @OA\Property(property="warrantyStartDate", type="string", description="تاریخ شروع گارانتی"),
* @OA\Property(property="warrantyEndDate", type="string", description="تاریخ پایان گارانتی"),
* @OA\Property(property="status", type="string", description="وضعیت"),
* @OA\Property(property="activation", type="string", description="فعال‌سازی"),
* @OA\Property(property="notes", type="string", description="یادداشت‌ها"),
* @OA\Property(property="expired", type="boolean", description="آیا منقضی شده"),
* @OA\Property(property="submitter", type="object",
* @OA\Property(property="id", type="integer", description="شناسه ثبت‌کننده"),
* @OA\Property(property="name", type="string", description="نام ثبت‌کننده")
* ),
* @OA\Property(property="commoditySerial", type="string", description="سریال کالا"),
* @OA\Property(property="buyer", type="object",
* @OA\Property(property="id", type="integer", description="شناسه خریدار"),
* @OA\Property(property="code", type="string", description="کد خریدار"),
* @OA\Property(property="name", type="string", description="نام خریدار"),
* @OA\Property(property="nikename", type="string", description="نام مستعار"),
* @OA\Property(property="mobile", type="string", description="موبایل")
* ),
* @OA\Property(property="activationAt", type="string", description="تاریخ فعال‌سازی"),
* @OA\Property(property="allocatedToDocumentCode", type="string", description="کد سند تخصیص داده شده")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=404, description="سریال یافت نشد")
* )
*/
#[Route('/api/plugins/warranty/serials/{id}', name: 'plugin_warranty_serial', methods: ['GET'])]
public function plugin_warranty_serial(EntityManagerInterface $entityManager, Access $access, $id): JsonResponse
{
@ -527,6 +933,41 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* افزودن سریال گارانتی جدید
*
* @OA\Post(
* path="/api/plugins/warranty/serials/add",
* summary="افزودن سریال گارانتی جدید",
* tags={"Warranty Serials"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"serialNumber", "commodity_id"},
* @OA\Property(property="serialNumber", type="string", description="شماره سریال"),
* @OA\Property(property="commodity_id", type="integer", description="شناسه کالا"),
* @OA\Property(property="description", type="string", description="توضیحات"),
* @OA\Property(property="warrantyStartDate", type="string", description="تاریخ شروع گارانتی"),
* @OA\Property(property="warrantyEndDate", type="string", description="تاریخ پایان گارانتی"),
* @OA\Property(property="status", type="string", description="وضعیت"),
* @OA\Property(property="notes", type="string", description="یادداشت‌ها")
* )
* ),
* @OA\Response(
* response=200,
* description="سریال با موفقیت افزوده شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="سریال با موفقیت افزوده شد"),
* @OA\Property(property="id", type="integer", description="شناسه سریال ایجاد شده")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="شماره سریال تکراری است یا محصول یافت نشد"),
* @OA\Response(response=404, description="کسب‌وکار یافت نشد")
* )
*/
#[Route('/api/plugins/warranty/serials/add', name: 'plugin_warranty_serial_add', methods: ['POST'])]
public function plugin_warranty_serial_add(Request $request, EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, Log $log): JsonResponse
{
@ -595,6 +1036,74 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* ویرایش سریال گارانتی
*
* @OA\Post(
* path="/api/plugins/warranty/serials/edit/{id}",
* summary="ویرایش اطلاعات سریال گارانتی",
* tags={"Warranty Serials"},
* @OA\Parameter(
* name="id",
* in="path",
* description="شناسه سریال گارانتی",
* required=true,
* @OA\Schema(type="integer")
* ),
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* @OA\Property(property="serialNumber", type="string", description="شماره سریال"),
* @OA\Property(property="commodity_id", type="integer", description="شناسه کالا"),
* @OA\Property(property="description", type="string", description="توضیحات"),
* @OA\Property(property="warrantyStartDate", type="string", description="تاریخ شروع گارانتی"),
* @OA\Property(property="warrantyEndDate", type="string", description="تاریخ پایان گارانتی"),
* @OA\Property(property="status", type="string", description="وضعیت"),
* @OA\Property(property="notes", type="string", description="یادداشت‌ها"),
* @OA\Property(property="activation", type="string", description="فعال‌سازی")
* )
* ),
* @OA\Response(
* response=200,
* description="سریال با موفقیت ویرایش شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="سریال با موفقیت ویرایش شد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="شماره سریال تکراری است یا محصول یافت نشد"),
* @OA\Response(response=404, description="سریال یافت نشد")
* )
*/
/**
* ویرایش سریال گارانتی
*
* @OA\Post(
* path="/api/plugins/warranty/serials/edit/{id}",
* summary="ویرایش اطلاعات سریال گارانتی",
* tags={"Warranty Serials"},
* @OA\Parameter(name="id", in="path", description="شناسه سریال", required=true, @OA\Schema(type="integer")),
* @OA\RequestBody(required=true, @OA\JsonContent(
* @OA\Property(property="serialNumber", type="string"),
* @OA\Property(property="commodity_id", type="integer"),
* @OA\Property(property="description", type="string"),
* @OA\Property(property="warrantyStartDate", type="string"),
* @OA\Property(property="warrantyEndDate", type="string"),
* @OA\Property(property="status", type="string"),
* @OA\Property(property="notes", type="string"),
* @OA\Property(property="activation", type="string")
* )),
* @OA\Response(response=200, description="سریال ویرایش شد", @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="سریال با موفقیت ویرایش شد")
* )),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="شماره سریال تکراری است"),
* @OA\Response(response=404, description="سریال یافت نشد")
* )
*/
#[Route('/api/plugins/warranty/serials/edit/{id}', name: 'plugin_warranty_serial_edit', methods: ['POST'])]
public function plugin_warranty_serial_edit(Request $request, EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, $id, Log $log): JsonResponse
{
@ -691,6 +1200,49 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* حذف سریال گارانتی
*
* @OA\Delete(
* path="/api/plugins/warranty/serials/{id}",
* summary="حذف سریال گارانتی",
* tags={"Warranty Serials"},
* @OA\Parameter(
* name="id",
* in="path",
* description="شناسه سریال گارانتی",
* required=true,
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="سریال با موفقیت حذف شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="سریال با موفقیت حذف شد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=404, description="سریال یافت نشد")
* )
*/
/**
* حذف سریال گارانتی
*
* @OA\Delete(
* path="/api/plugins/warranty/serials/{id}",
* summary="حذف سریال گارانتی",
* tags={"Warranty Serials"},
* @OA\Parameter(name="id", in="path", description="شناسه سریال", required=true, @OA\Schema(type="integer")),
* @OA\Response(response=200, description="سریال حذف شد", @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="سریال با موفقیت حذف شد")
* )),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=404, description="سریال یافت نشد")
* )
*/
#[Route('/api/plugins/warranty/serials/{id}', name: 'plugin_warranty_serial_delete', methods: ['DELETE'])]
public function plugin_warranty_serial_delete(EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, $id, Log $log): JsonResponse
{
@ -729,6 +1281,69 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* وارد کردن انبوه سریال‌های گارانتی
*
* @OA\Post(
* path="/api/plugins/warranty/serials/bulk-import",
* summary="وارد کردن انبوه سریال‌های گارانتی",
* tags={"Warranty Import"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"serials"},
* @OA\Property(property="serials", type="array", @OA\Items(
* type="object",
* @OA\Property(property="serialNumber", type="string", description="شماره سریال"),
* @OA\Property(property="commodity_code", type="string", description="کد کالا"),
* @OA\Property(property="description", type="string", description="توضیحات"),
* @OA\Property(property="warrantyStartDate", type="string", description="تاریخ شروع گارانتی"),
* @OA\Property(property="warrantyEndDate", type="string", description="تاریخ پایان گارانتی"),
* @OA\Property(property="status", type="string", description="وضعیت"),
* @OA\Property(property="notes", type="string", description="یادداشت‌ها")
* ), description="لیست سریال‌ها برای وارد کردن")
* )
* ),
* @OA\Response(
* response=200,
* description="عملیات وارد کردن انبوه تکمیل شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", description="پیام نتیجه"),
* @OA\Property(property="successCount", type="integer", description="تعداد موفق"),
* @OA\Property(property="errorCount", type="integer", description="تعداد خطا"),
* @OA\Property(property="errors", type="array", @OA\Items(type="string"), description="لیست خطاها")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="داده‌های نامعتبر"),
* @OA\Response(response=404, description="کسب‌وکار یافت نشد")
* )
*/
/**
* وارد کردن انبوه سریال‌های گارانتی
*
* @OA\Post(
* path="/api/plugins/warranty/serials/bulk-import",
* summary="وارد کردن انبوه سریال‌های گارانتی",
* tags={"Warranty Import"},
* @OA\RequestBody(required=true, @OA\JsonContent(
* required={"serials"},
* @OA\Property(property="serials", type="array", @OA\Items(type="object"), description="لیست سریال‌ها")
* )),
* @OA\Response(response=200, description="عملیات تکمیل شد", @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string"),
* @OA\Property(property="successCount", type="integer"),
* @OA\Property(property="errorCount", type="integer"),
* @OA\Property(property="errors", type="array", @OA\Items(type="string"))
* )),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="داده‌های نامعتبر"),
* @OA\Response(response=404, description="کسب‌وکار یافت نشد")
* )
*/
#[Route('/api/plugins/warranty/serials/bulk-import', name: 'plugin_warranty_serial_bulk_import', methods: ['POST'])]
public function plugin_warranty_serial_bulk_import(Request $request, EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, Log $log): JsonResponse
{
@ -829,6 +1444,26 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* دریافت آمار سریال‌های گارانتی
*
* @OA\Get(
* path="/api/plugins/warranty/stats",
* summary="دریافت آمار و گزارش سریال‌های گارانتی",
* tags={"Warranty Statistics"},
* @OA\Response(
* response=200,
* description="آمار سریال‌های گارانتی",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="totalSerials", type="integer", description="تعداد کل سریال‌ها"),
* @OA\Property(property="byStatus", type="object", description="تعداد بر اساس وضعیت"),
* @OA\Property(property="expiredFlagCount", type="integer", description="تعداد منقضی شده")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز")
* )
*/
#[Route('/api/plugins/warranty/stats', name: 'plugin_warranty_stats', methods: ['GET'])]
public function plugin_warranty_stats(EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access): JsonResponse
{
@ -874,6 +1509,35 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* حذف گروهی سریال‌های گارانتی
*
* @OA\Post(
* path="/api/plugins/warranty/serials/bulk-delete",
* summary="حذف گروهی سریال‌های گارانتی",
* tags={"Warranty Serials"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"ids"},
* @OA\Property(property="ids", type="array", @OA\Items(type="integer"), description="شناسه‌های سریال‌ها برای حذف")
* )
* ),
* @OA\Response(
* response=200,
* description="سریال‌ها با موفقیت حذف شدند",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", description="پیام نتیجه"),
* @OA\Property(property="deletedCount", type="integer", description="تعداد حذف شده"),
* @OA\Property(property="errors", type="array", @OA\Items(type="string"), description="لیست خطاها")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="هیچ آیتمی برای حذف انتخاب نشده است")
* )
*/
#[Route('/api/plugins/warranty/serials/bulk-delete', name: 'plugin_warranty_serial_bulk_delete', methods: ['POST'])]
public function plugin_warranty_serial_bulk_delete(Request $request, EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, Log $log): JsonResponse
{
@ -937,17 +1601,39 @@ class PlugWarrantyController extends AbstractController
}
}
/**
* دریافت تنظیمات گارانتی
*
* @OA\Get(
* path="/api/plugins/warranty/settings/get",
* summary="دریافت تنظیمات گارانتی کسب و کار",
* tags={"Warranty Settings"},
* @OA\Response(
* response=200,
* description="تنظیمات گارانتی",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="requireWarrantyOnDelivery", type="boolean", description="آیا گارانتی در تحویل الزامی است"),
* @OA\Property(property="activationGraceDays", type="integer", description="تعداد روزهای مهلت فعال‌سازی"),
* @OA\Property(property="matchWarrantyToSerial", type="boolean", description="آیا گارانتی با سریال مطابقت دارد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز")
* )
*/
#[Route('/api/plugins/warranty/settings/get', name: 'plugin_warranty_settings_get', methods: ['GET'])]
public function plugin_warranty_settings_get(Access $access, registryMGR $registryMGR): JsonResponse
public function plugin_warranty_settings_get(Access $access, registryMGR $registryMGR, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('plugWarrantyManager');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$require = filter_var($registryMGR->get('warranty', 'requireWarrantyOnDelivery'), FILTER_VALIDATE_BOOLEAN);
$grace = (int) ($registryMGR->get('warranty', 'activationGraceDays') ?? 7);
$match = filter_var($registryMGR->get('warranty', 'matchWarrantyToSerial'), FILTER_VALIDATE_BOOLEAN);
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
$require = filter_var($business->getRequireWarrantyOnDelivery(), FILTER_VALIDATE_BOOLEAN);
$grace = (int) ($business->getActivationGraceDays() ?? 7);
$match = filter_var($business->getMatchWarrantyToSerial(), FILTER_VALIDATE_BOOLEAN);
return $this->json([
'requireWarrantyOnDelivery' => (bool) $require,
'activationGraceDays' => max(0, $grace),
@ -955,23 +1641,54 @@ class PlugWarrantyController extends AbstractController
]);
}
/**
* ذخیره تنظیمات گارانتی
*
* @OA\Post(
* path="/api/plugins/warranty/settings/save",
* summary="ذخیره تنظیمات گارانتی کسب و کار",
* tags={"Warranty Settings"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* @OA\Property(property="requireWarrantyOnDelivery", type="boolean", description="آیا گارانتی در تحویل الزامی است"),
* @OA\Property(property="activationGraceDays", type="integer", description="تعداد روزهای مهلت فعال‌سازی"),
* @OA\Property(property="matchWarrantyToSerial", type="boolean", description="آیا گارانتی با سریال مطابقت دارد")
* )
* ),
* @OA\Response(
* response=200,
* description="تنظیمات با موفقیت ذخیره شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true)
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز")
* )
*/
#[Route('/api/plugins/warranty/settings/save', name: 'plugin_warranty_settings_save', methods: ['POST'])]
public function plugin_warranty_settings_save(Request $request, Access $access, registryMGR $registryMGR): JsonResponse
public function plugin_warranty_settings_save(Request $request, Access $access, registryMGR $registryMGR, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('plugWarrantyManager');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
$params = json_decode($request->getContent() ?: '{}', true);
$require = isset($params['requireWarrantyOnDelivery']) && ($params['requireWarrantyOnDelivery'] === true || $params['requireWarrantyOnDelivery'] === '1' || $params['requireWarrantyOnDelivery'] === 1 || $params['requireWarrantyOnDelivery'] === 'true');
$graceDays = isset($params['activationGraceDays']) ? (int) $params['activationGraceDays'] : 7;
if ($graceDays < 0) { $graceDays = 0; }
$match = isset($params['matchWarrantyToSerial']) && ($params['matchWarrantyToSerial'] === true || $params['matchWarrantyToSerial'] === '1' || $params['matchWarrantyToSerial'] === 1 || $params['matchWarrantyToSerial'] === 'true');
$registryMGR->update('warranty', 'requireWarrantyOnDelivery', $require ? '1' : '0');
$registryMGR->update('warranty', 'activationGraceDays', (string) $graceDays);
$registryMGR->update('warranty', 'matchWarrantyToSerial', $match ? '1' : '0');
$business->setRequireWarrantyOnDelivery($require);
$business->setActivationGraceDays($graceDays);
$business->setMatchWarrantyToSerial($match);
$entityManager->flush();
return $this->json(['success' => true]);
}

View file

@ -3,12 +3,13 @@
/**
* Developed by Mohammad Rezai
* https://pirouz.xyz 2025-07-28
*/
*/
namespace App\Controller\Plugins;
use App\Entity\Business;
use App\Entity\Permission;
use App\Service\Access;
use App\Service\Extractor;
use App\Service\Log;
use App\Service\registryMGR;
use Doctrine\ORM\EntityManagerInterface;
@ -21,17 +22,78 @@ use App\Entity\HesabdariDoc;
use App\Entity\PluginTaxInvoice;
use App\Dto\TaxSettingsDto;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use App\Entity\User;
use OpenApi\Annotations as OA;
use DateTime;
class TaxSettingsController extends AbstractController
{
private function getMoadianBaseUrl(registryMGR $registryMGR): string
{
$sandboxMode = filter_var($registryMGR->get('system_settings', 'tax_system_sandbox_mode'), FILTER_VALIDATE_BOOLEAN);
return $sandboxMode ? 'https://sandboxrc.tax.gov.ir/' : 'https://tp.tax.gov.ir/';
}
private function getTaxSettings(EntityManagerInterface $em, int $businessId, User $user): array
{
$perm = $em->getRepository(Permission::class)->findOneBy([
'bid' => $businessId,
'user' => $user
]);
$business = $em->getRepository(Business::class)->find($businessId);
if ($business->getOwner() == $user) {
$repo = $em->getRepository(PluginTaxsettingsKey::class);
$entity = $repo->findOneBy(['business_id' => $businessId]);
$settings = [
'taxMemoryId' => $entity ? $entity->getTaxMemoryId() : '',
'economicCode' => $entity ? $entity->getEconomicCode() : '',
'privateKey' => $entity ? $entity->getPrivateKey() : '',
];
} else {
if (!$perm || !$perm->isPlugTaxSettings()) {
return [
'success' => false,
'message' => 'شما دسترسی لازم را ندارید.'
];
}
$repo = $em->getRepository(PluginTaxsettingsKey::class);
$entity = $repo->findOneBy(['business_id' => $businessId]);
$settings = [
'taxMemoryId' => $entity ? $entity->getTaxMemoryId() : '',
'economicCode' => $entity ? $entity->getEconomicCode() : '',
'privateKey' => $entity ? $entity->getPrivateKey() : '',
];
}
return $settings;
}
/**
* دریافت تنظیمات مالیاتی
*
* @OA\Get(
* path="/api/plugins/tax/settings/get",
* summary="دریافت تنظیمات مالیاتی کسب و کار",
* tags={"Tax Settings"},
* @OA\Response(
* response=200,
* description="تنظیمات مالیاتی",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="taxMemoryId", type="string", description="شناسه حافظه مالیاتی"),
* @OA\Property(property="economicCode", type="string", description="کد اقتصادی"),
* @OA\Property(property="privateKey", type="string", description="کلید خصوصی")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز")
* )
*/
#[Route('/api/plugins/tax/settings/get', name: 'plugin_tax_settings_get', methods: ['GET'])]
public function plugin_tax_settings_get(EntityManagerInterface $em, Access $access): JsonResponse
{
@ -42,20 +104,41 @@ class TaxSettingsController extends AbstractController
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$user = $this->getUser();
$userId = $user instanceof \App\Entity\User ? $user->getId() : null;
$repo = $em->getRepository(PluginTaxsettingsKey::class);
$entity = $repo->findOneBy(['business_id' => $businessId, 'user_id' => $userId]);
$settings = [
'taxMemoryId' => $entity ? $entity->getTaxMemoryId() : '',
'economicCode' => $entity ? $entity->getEconomicCode() : '',
'privateKey' => $entity ? $entity->getPrivateKey() : '',
];
$settings = $this->getTaxSettings($em, $businessId, $user);
return $this->json($settings);
}
/**
* ذخیره تنظیمات مالیاتی
*
* @OA\Post(
* path="/api/plugins/tax/settings/save",
* summary="ذخیره تنظیمات مالیاتی کسب و کار",
* tags={"Tax Settings"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"taxMemoryId", "economicCode", "privateKey"},
* @OA\Property(property="taxMemoryId", type="string", description="شناسه حافظه مالیاتی"),
* @OA\Property(property="economicCode", type="string", description="کد اقتصادی"),
* @OA\Property(property="privateKey", type="string", description="کلید خصوصی")
* )
* ),
* @OA\Response(
* response=200,
* description="تنظیمات با موفقیت ذخیره شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="تنظیمات با موفقیت ذخیره شد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=422, description="اطلاعات نامعتبر")
* )
*/
#[Route('/api/plugins/tax/settings/save', name: 'plugin_tax_settings_save', methods: ['POST'])]
public function plugin_tax_settings_save(Request $request, registryMGR $registryMGR, Access $access, Log $log, EntityManagerInterface $em, ValidatorInterface $validator): JsonResponse
{
@ -85,7 +168,7 @@ class TaxSettingsController extends AbstractController
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$user = $this->getUser();
$userId = $user instanceof \App\Entity\User ? $user->getId() : null;
$userId = $user instanceof User ? $user->getId() : null;
$repo = $em->getRepository(PluginTaxsettingsKey::class);
$entity = $repo->findOneBy(['business_id' => $businessId, 'user_id' => $userId]);
@ -93,12 +176,12 @@ class TaxSettingsController extends AbstractController
$entity = new PluginTaxsettingsKey();
$entity->setBusinessId($businessId);
$entity->setUserId($userId);
$entity->setCreatedAt(new \DateTime());
$entity->setCreatedAt(new DateTime());
}
$entity->setPrivateKey($dto->privateKey);
$entity->setTaxMemoryId($dto->taxMemoryId);
$entity->setEconomicCode($dto->economicCode);
$entity->setUpdatedAt(new \DateTime());
$entity->setUpdatedAt(new DateTime());
$em->persist($entity);
$em->flush();
@ -143,6 +226,40 @@ class TaxSettingsController extends AbstractController
return $keyDetails['key'];
}
/**
* تولید کلید و CSR
*
* @OA\Post(
* path="/api/plugins/tax/settings/generate-csr",
* summary="تولید کلید خصوصی، عمومی و CSR برای سامانه مودیان",
* tags={"Tax Settings"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"nationalId"},
* @OA\Property(property="personType", type="string", enum={"natural", "legal"}, description="نوع شخص"),
* @OA\Property(property="nationalId", type="string", description="شناسه ملی"),
* @OA\Property(property="nameFa", type="string", description="نام فارسی (برای اشخاص حقوقی)"),
* @OA\Property(property="nameEn", type="string", description="نام انگلیسی (برای اشخاص حقوقی)"),
* @OA\Property(property="email", type="string", description="ایمیل (برای اشخاص حقوقی)")
* )
* ),
* @OA\Response(
* response=200,
* description="کلید و CSR با موفقیت تولید شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="privateKey", type="string", description="کلید خصوصی"),
* @OA\Property(property="publicKey", type="string", description="کلید عمومی"),
* @OA\Property(property="csr", type="string", description="CSR (برای اشخاص حقوقی)"),
* @OA\Property(property="message", type="string", description="پیام نتیجه")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="اطلاعات نامعتبر")
* )
*/
#[Route('/api/plugins/tax/settings/generate-csr', name: 'plugin_tax_settings_generate_csr', methods: ['POST'])]
public function plugin_tax_settings_generate_csr(Request $request, registryMGR $registryMGR, Access $access, Log $log): JsonResponse
{
@ -154,14 +271,14 @@ class TaxSettingsController extends AbstractController
$params = $request->getPayload()->all();
$personType = $params['personType'] ?? 'natural';
if (empty($params['nationalId'])) {
return $this->json([
'success' => false,
'message' => 'شناسه ملی الزامی است'
]);
}
if ($personType === 'legal') {
if (empty($params['nameFa']) || empty($params['nameEn']) || empty($params['email'])) {
return $this->json([
@ -174,15 +291,15 @@ class TaxSettingsController extends AbstractController
try {
$privateKey = $this->generatePrivateKey();
$publicKey = $this->generatePublicKey($privateKey);
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$response = [
'success' => true,
'privateKey' => $privateKey,
'publicKey' => $publicKey
];
if ($personType === 'legal') {
$csr = $this->generateCSR($privateKey, $params);
$response['csr'] = $csr;
@ -245,6 +362,39 @@ class TaxSettingsController extends AbstractController
return $csr;
}
/**
* اضافه کردن فاکتورها به لیست ارسال
*
* @OA\Post(
* path="/api/plugins/tax/list/send-invoice",
* summary="اضافه کردن فاکتورها به لیست ارسال به سامانه مودیان",
* tags={"Tax Invoices"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"codes"},
* @OA\Property(property="codes", type="array", @OA\Items(type="string"), description="کدهای فاکتور")
* )
* ),
* @OA\Response(
* response=200,
* description="فاکتورها با موفقیت به لیست ارسال اضافه شدند",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", description="پیام نتیجه"),
* @OA\Property(property="summary", type="object",
* @OA\Property(property="total", type="integer", description="تعداد کل"),
* @OA\Property(property="success", type="integer", description="تعداد موفق"),
* @OA\Property(property="error", type="integer", description="تعداد خطا")
* ),
* @OA\Property(property="results", type="array", @OA\Items(type="object"))
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="اطلاعات نامعتبر")
* )
*/
#[Route('/api/plugins/tax/list/send-invoice', name: 'plugin_tax_list_send_invoice', methods: ['POST'])]
public function plugin_tax_list_send_invoice(Request $request, Access $access, Log $log, EntityManagerInterface $em): JsonResponse
{
@ -265,7 +415,7 @@ class TaxSettingsController extends AbstractController
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$user = $this->getUser();
$userId = $user instanceof \App\Entity\User ? $user->getId() : null;
$userId = $user instanceof User ? $user->getId() : null;
try {
$taxRepo = $em->getRepository(PluginTaxsettingsKey::class);
@ -382,8 +532,8 @@ class TaxSettingsController extends AbstractController
}
$taxInvoice = new PluginTaxInvoice();
$taxInvoice->setBusiness($em->getRepository(\App\Entity\Business::class)->find($businessId));
$taxInvoice->setUser($em->getRepository(\App\Entity\User::class)->find($userId));
$taxInvoice->setBusiness($em->getRepository(Business::class)->find($businessId));
$taxInvoice->setUser($em->getRepository(User::class)->find($userId));
$taxInvoice->setInvoice($invoice);
$taxInvoice->setInvoiceCode($invoice->getCode());
$taxInvoice->setStatus('pending');
@ -427,6 +577,40 @@ class TaxSettingsController extends AbstractController
}
}
/**
* دریافت لیست فاکتورهای مالیاتی
*
* @OA\Get(
* path="/api/plugins/tax/invoices/list",
* summary="دریافت لیست فاکتورهای مالیاتی کسب و کار",
* tags={"Tax Invoices"},
* @OA\Response(
* response=200,
* description="لیست فاکتورهای مالیاتی",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="data", type="array", @OA\Items(
* type="object",
* @OA\Property(property="id", type="integer", description="شناسه فاکتور مالیاتی"),
* @OA\Property(property="invoiceNumber", type="string", description="شماره فاکتور"),
* @OA\Property(property="date", type="string", description="تاریخ فاکتور"),
* @OA\Property(property="customerName", type="string", description="نام مشتری"),
* @OA\Property(property="customerId", type="string", description="کد مشتری"),
* @OA\Property(property="totalAmount", type="number", description="مبلغ کل"),
* @OA\Property(property="status", type="string", description="وضعیت"),
* @OA\Property(property="sentDate", type="string", description="تاریخ ارسال"),
* @OA\Property(property="errorMessage", type="string", description="پیام خطا"),
* @OA\Property(property="createdAt", type="string", description="تاریخ ایجاد"),
* @OA\Property(property="uniqueTaxNumber", type="string", description="شماره یکتا مالیاتی"),
* @OA\Property(property="referenceUniqueTaxNumber", type="string", description="شماره ارجاع یکتا"),
* @OA\Property(property="invoiceType", type="string", description="نوع فاکتور")
* ))
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز")
* )
*/
#[Route('/api/plugins/tax/invoices/list', name: 'plugin_tax_settings_invoices_list', methods: ['GET'])]
public function plugin_tax_settings_invoices_list(Request $request, Access $access, EntityManagerInterface $em): JsonResponse
{
@ -555,6 +739,7 @@ class TaxSettingsController extends AbstractController
$itemDiscountType = $row->getDiscountType() ?? 'fixed';
$itemDiscountPercent = $row->getDiscountPercent() ?? 0;
$itemTax = $row->getTax() ?? 0;
$count = $row->getCommdityCount() ?? 0;
if ($itemDiscountType === 'percent' && $itemDiscountPercent > 0) {
$originalPrice = $basePrice / (1 - ($itemDiscountPercent / 100));
@ -562,8 +747,8 @@ class TaxSettingsController extends AbstractController
} else {
$originalPrice = $basePrice + $itemDiscount;
}
$unitPrice = $row->getCommdityCount() > 0 ? $originalPrice / $row->getCommdityCount() : 0;
$unitPrice = $count > 0 ? $originalPrice / $count : 0;
$es = $count > 0 ? ($unitPrice * $count) : $originalPrice;
$netPrice = $basePrice;
$totalInvoice += $netPrice;
@ -574,8 +759,9 @@ class TaxSettingsController extends AbstractController
'name' => $row->getCommodity()->getName(),
'code' => $row->getCommodity()->getCode()
],
'count' => $row->getCommdityCount(),
'count' => $count,
'price' => $unitPrice,
'prdis' => $es,
'discountPercent' => $itemDiscountPercent,
'discountAmount' => $itemDiscount,
'total' => $netPrice,
@ -663,7 +849,7 @@ class TaxSettingsController extends AbstractController
$rowNumber = 1;
foreach ($data['items'] as $item) {
$commodity = $item['name'];
if (empty($commodity['code'])) {
$errors[] = "ردیف {$rowNumber}: کد کالا/خدمت تعریف نشده است";
}
@ -738,6 +924,36 @@ class TaxSettingsController extends AbstractController
}
}
/**
* ارسال فاکتور مالیاتی
*
* @OA\Post(
* path="/api/plugins/tax/invoice/send/{id}",
* summary="ارسال فاکتور مالیاتی به سامانه مودیان",
* tags={"Tax Invoices"},
* @OA\Parameter(
* name="id",
* in="path",
* description="شناسه فاکتور مالیاتی",
* required=true,
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="فاکتور با موفقیت ارسال شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="data", type="object", description="پاسخ سامانه مودیان"),
* @OA\Property(property="invoiceCode", type="string", description="کد فاکتور"),
* @OA\Property(property="referenceNumber", type="string", description="شماره ارجاع")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=404, description="فاکتور مالیاتی یافت نشد"),
* @OA\Response(response=400, description="خطا در ارسال فاکتور")
* )
*/
#[Route('/api/plugins/tax/invoice/send/{id}', name: 'plugin_tax_invoice_send', methods: ['POST'])]
public function sendTaxInvoice(int $id, Access $access, Log $log, EntityManagerInterface $em, registryMGR $registryMGR): JsonResponse
{
@ -748,7 +964,6 @@ class TaxSettingsController extends AbstractController
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$user = $this->getUser();
$userId = $user instanceof \App\Entity\User ? $user->getId() : null;
$taxInvoiceRepo = $em->getRepository(PluginTaxInvoice::class);
$taxInvoice = $taxInvoiceRepo->findOneBy([
@ -772,10 +987,9 @@ class TaxSettingsController extends AbstractController
]);
}
$repo = $em->getRepository(PluginTaxsettingsKey::class);
$taxSettings = $repo->findOneBy(['business_id' => $businessId, 'user_id' => $userId]);
$taxSettings = $this->getTaxSettings($em, $businessId, $user);
if (!$taxSettings || !$taxSettings->getPrivateKey() || !$taxSettings->getTaxMemoryId()) {
if (!$taxSettings['privateKey'] || !$taxSettings['taxMemoryId']) {
return $this->json([
'success' => false,
'message' => 'تنظیمات مالیاتی تکمیل نشده است. لطفاً ابتدا تنظیمات را تکمیل کنید.'
@ -783,8 +997,8 @@ class TaxSettingsController extends AbstractController
}
try {
$username = $taxSettings->getTaxMemoryId();
$privateKey = $taxSettings->getPrivateKey();
$username = $taxSettings['taxMemoryId'];
$privateKey = $taxSettings['privateKey'];
if (!$username || !$privateKey) {
return $this->json([
@ -832,22 +1046,22 @@ class TaxSettingsController extends AbstractController
$moadian->setToken($token);
$invoice = $taxInvoice->getInvoice();
try {
if (!$invoice) {
throw new \Exception('فاکتور معتبر نیست');
}
$validationResult = $this->validateInvoiceForTax($invoice);
if (!$validationResult['valid']) {
throw new \Exception($validationResult['message']);
}
$invoiceDto = $this->buildInvoiceDto($invoice, $moadian, $taxSettings->getEconomicCode());
if (!$invoiceDto) {
$invoiceDto = $this->buildInvoiceDto($invoice, $moadian, $taxSettings['economicCode']);
if (!$invoiceDto) {
throw new \Exception('خطا در آماده‌سازی فاکتور: خطا در ساخت DTO فاکتور');
}
$response = $moadian->sendInvoices([$invoiceDto]);
} catch (\Exception $e) {
return $this->json([
@ -899,6 +1113,34 @@ class TaxSettingsController extends AbstractController
}
}
/**
* حذف فاکتور مالیاتی
*
* @OA\Delete(
* path="/api/plugins/tax/invoice/delete/{id}",
* summary="حذف فاکتور مالیاتی از لیست ارسال",
* tags={"Tax Invoices"},
* @OA\Parameter(
* name="id",
* in="path",
* description="شناسه فاکتور مالیاتی",
* required=true,
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="فاکتور مالیاتی با موفقیت حذف شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="فاکتور مالیاتی با موفقیت حذف شد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=404, description="فاکتور مالیاتی یافت نشد"),
* @OA\Response(response=400, description="فاکتور قابل حذف نیست")
* )
*/
#[Route('/api/plugins/tax/invoice/delete/{id}', name: 'plugin_tax_invoice_delete', methods: ['DELETE'])]
public function deleteTaxInvoice(int $id, Access $access, Log $log, EntityManagerInterface $em): JsonResponse
{
@ -955,6 +1197,33 @@ class TaxSettingsController extends AbstractController
}
}
/**
* استعلام وضعیت فاکتورهای مالیاتی
*
* @OA\Post(
* path="/api/plugins/tax/inquire-status",
* summary="استعلام وضعیت فاکتورهای ارسال شده به سامانه مودیان",
* tags={"Tax Invoices"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"referenceNumbers"},
* @OA\Property(property="referenceNumbers", type="array", @OA\Items(type="string"), description="شماره‌های ارجاع فاکتورها")
* )
* ),
* @OA\Response(
* response=200,
* description="وضعیت فاکتورها با موفقیت دریافت شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="data", type="array", @OA\Items(type="object"), description="اطلاعات وضعیت فاکتورها")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="اطلاعات نامعتبر")
* )
*/
#[Route('/api/plugins/tax/inquire-status', name: 'plugin_tax_inquire_status', methods: ['POST'])]
public function inquireInvoiceStatus(Request $request, Access $access, EntityManagerInterface $em, registryMGR $registryMGR): JsonResponse
{
@ -975,12 +1244,10 @@ class TaxSettingsController extends AbstractController
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$user = $this->getUser();
$userId = $user instanceof \App\Entity\User ? $user->getId() : null;
$repo = $em->getRepository(PluginTaxsettingsKey::class);
$taxSettings = $repo->findOneBy(['business_id' => $businessId, 'user_id' => $userId]);
$taxSettings = $this->getTaxSettings($em, $businessId, $user);
if (!$taxSettings || !$taxSettings->getPrivateKey() || !$taxSettings->getTaxMemoryId()) {
if (!$taxSettings['privateKey'] || !$taxSettings['taxMemoryId']) {
return $this->json([
'success' => false,
'message' => 'تنظیمات مالیاتی تکمیل نشده است. لطفاً ابتدا تنظیمات را تکمیل کنید.'
@ -988,8 +1255,8 @@ class TaxSettingsController extends AbstractController
}
try {
$username = $taxSettings->getTaxMemoryId();
$privateKey = $taxSettings->getPrivateKey();
$username = $taxSettings['taxMemoryId'];
$privateKey = $taxSettings['privateKey'];
$taxOrgPublicKey = '';
$taxOrgKeyId = '';
@ -1111,7 +1378,7 @@ class TaxSettingsController extends AbstractController
$vra = round(($itemTax / $itemTotal) * 100, 2);
$invoiceType = $invoice->getType() ?? 'sell';
switch ($invoiceType) {
case 'return_sell':
case 'return_buy':
@ -1165,7 +1432,7 @@ class TaxSettingsController extends AbstractController
$buyerNationalId = null;
$buyerEconomicCode = null;
$buyerPostalCode = null;
$buyerPerson = null;
foreach ($invoice->getHesabdariRows() as $row) {
if ($row->getPerson()) {
@ -1182,7 +1449,7 @@ class TaxSettingsController extends AbstractController
if (empty($buyerNationalId) || trim($buyerNationalId) === '') {
$buyerNationalId = null;
}
if (empty($buyerEconomicCode) || trim($buyerEconomicCode) === '') {
$buyerEconomicCode = null;
}
@ -1193,7 +1460,7 @@ class TaxSettingsController extends AbstractController
}
$personType = 1;
if (strlen($buyerNationalId) == 11) {
$personType = 2;
}
@ -1229,7 +1496,7 @@ class TaxSettingsController extends AbstractController
->setScc(null)
->setCrn(null)
->setBillid(null)
->setTprdis($data['totalInvoice'])
->setTprdis(array_sum(array_column($data['items'], 'prdis')))
->setTdis($data['totalDiscount'])
->setTadis($data['totalInvoice'] - $data['totalDiscount'])
->setTvam($totalTax)
@ -1241,13 +1508,23 @@ class TaxSettingsController extends AbstractController
->setTvop(null)
->setTax17(0);
$bodyItems = [];
foreach ($data['items'] as $item) {
$itemTax = $item['tax'];
$itemTotal = $item['total'] + $itemTax;
$vra = $this->calculateVra($item['total'], $itemTax, $invoice);
$prdis = $item['count'] * $item['price'];
$adis = $prdis - $item['discountAmount'];
$ks = ($adis * $vra) / 100;
$ks2 = 0;
$ks3 = 0;
$os = $adis + $ks + $ks2 + $ks3;
$bodyDto = (new \SnappMarketPro\Moadian\Dto\InvoiceBodyDto())
->setSstid($this->getCommodityTaxCodeFromInvoice($invoice, $item['name']['id']))
->setSstt($item['name']['name'])
@ -1257,11 +1534,11 @@ class TaxSettingsController extends AbstractController
->setCfee(null)
->setCut(null)
->setExr(null)
->setPrdis($item['total'])
->setPrdis($prdis)
->setDis($item['discountAmount'])
->setAdis($item['total'] - $item['discountAmount'])
->setAdis($adis)
->setVra($vra)
->setVam($itemTax)
->setVam($ks)
->setOdt(null)
->setOdr(null)
->setOdam(null)
@ -1275,7 +1552,7 @@ class TaxSettingsController extends AbstractController
->setCop(null)
->setVop(null)
->setBsrn(null)
->setTsstam($itemTotal);
->setTsstam($os);
$bodyItems[] = $bodyDto;
}
@ -1300,6 +1577,39 @@ class TaxSettingsController extends AbstractController
}
}
/**
* ارسال گروهی فاکتورهای مالیاتی
*
* @OA\Post(
* path="/api/plugins/tax/invoice/send-bulk",
* summary="ارسال گروهی فاکتورهای مالیاتی به سامانه مودیان",
* tags={"Tax Invoices"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"ids"},
* @OA\Property(property="ids", type="array", @OA\Items(type="integer"), description="شناسه‌های فاکتورهای مالیاتی")
* )
* ),
* @OA\Response(
* response=200,
* description="ارسال گروهی فاکتورها تکمیل شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", description="پیام نتیجه"),
* @OA\Property(property="summary", type="object",
* @OA\Property(property="total", type="integer", description="تعداد کل"),
* @OA\Property(property="success", type="integer", description="تعداد موفق"),
* @OA\Property(property="error", type="integer", description="تعداد خطا")
* ),
* @OA\Property(property="results", type="array", @OA\Items(type="object"), description="نتایج ارسال هر فاکتور")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=400, description="اطلاعات نامعتبر")
* )
*/
#[Route('/api/plugins/tax/invoice/send-bulk', name: 'plugin_tax_invoice_send_bulk', methods: ['POST'])]
public function sendBulkTaxInvoices(Request $request, Access $access, Log $log, EntityManagerInterface $em, registryMGR $registryMGR): JsonResponse
{
@ -1320,12 +1630,10 @@ class TaxSettingsController extends AbstractController
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$user = $this->getUser();
$userId = $user instanceof \App\Entity\User ? $user->getId() : null;
$repo = $em->getRepository(PluginTaxsettingsKey::class);
$taxSettings = $repo->findOneBy(['business_id' => $businessId, 'user_id' => $userId]);
$taxSettings = $this->getTaxSettings($em, $businessId, $user);
if (!$taxSettings || !$taxSettings->getPrivateKey() || !$taxSettings->getTaxMemoryId()) {
if (!$taxSettings['privateKey'] || !$taxSettings['taxMemoryId']) {
return $this->json([
'success' => false,
'message' => 'تنظیمات مالیاتی تکمیل نشده است. لطفاً ابتدا تنظیمات را تکمیل کنید.'
@ -1333,8 +1641,8 @@ class TaxSettingsController extends AbstractController
}
try {
$username = $taxSettings->getTaxMemoryId();
$privateKey = $taxSettings->getPrivateKey();
$username = $taxSettings['taxMemoryId'];
$privateKey = $taxSettings['privateKey'];
if (!$username || !$privateKey) {
return $this->json([
@ -1419,7 +1727,7 @@ class TaxSettingsController extends AbstractController
}
$invoice = $taxInvoice->getInvoice();
if (!$invoice) {
$results[] = [
'id' => $id,
@ -1430,7 +1738,7 @@ class TaxSettingsController extends AbstractController
$errorCount++;
continue;
}
$validationResult = $this->validateInvoiceForTax($invoice);
if (!$validationResult['valid']) {
$results[] = [
@ -1442,9 +1750,9 @@ class TaxSettingsController extends AbstractController
$errorCount++;
continue;
}
$invoiceDto = $this->buildInvoiceDto($invoice, $moadian, $taxSettings->getEconomicCode());
if (!$invoiceDto) {
$invoiceDto = $this->buildInvoiceDto($invoice, $moadian, $taxSettings['economicCode']);
if (!$invoiceDto) {
$results[] = [
'id' => $id,
'code' => $taxInvoice->getInvoiceCode(),
@ -1454,7 +1762,7 @@ class TaxSettingsController extends AbstractController
$errorCount++;
continue;
}
$response = $moadian->sendInvoices([$invoiceDto]);
if (isset($response['result'][0]['referenceNumber']) && !empty($response['result'])) {
@ -1532,6 +1840,44 @@ class TaxSettingsController extends AbstractController
}
}
/**
* اعتبارسنجی اطلاعات خریدار
*
* @OA\Post(
* path="/api/plugins/tax/invoice/validate-buyer-info/{id}",
* summary="اعتبارسنجی اطلاعات اقتصادی خریدار فاکتور",
* tags={"Tax Invoices"},
* @OA\Parameter(
* name="id",
* in="path",
* description="شناسه فاکتور مالیاتی",
* required=true,
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="اطلاعات اقتصادی خریدار بررسی شد",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", description="پیام نتیجه"),
* @OA\Property(property="buyer_info", type="object",
* @OA\Property(property="is_valid", type="boolean", description="آیا اطلاعات معتبر است"),
* @OA\Property(property="buyer_name", type="string", description="نام خریدار"),
* @OA\Property(property="buyer_id", type="integer", description="شناسه خریدار"),
* @OA\Property(property="buyer_code", type="string", description="کد خریدار"),
* @OA\Property(property="national_id", type="string", description="شناسه ملی"),
* @OA\Property(property="economic_code", type="string", description="کد اقتصادی"),
* @OA\Property(property="missing_fields", type="array", @OA\Items(type="string"), description="فیلدهای ناقص")
* ),
* @OA\Property(property="can_proceed", type="boolean", description="آیا می‌توان ادامه داد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز"),
* @OA\Response(response=404, description="فاکتور مالیاتی یافت نشد"),
* @OA\Response(response=400, description="اطلاعات نامعتبر")
* )
*/
#[Route('/api/plugins/tax/invoice/validate-buyer-info/{id}', name: 'plugin_tax_invoice_validate_buyer_info', methods: ['POST'])]
public function validateBuyerInfo(int $id, Access $access, Log $log, EntityManagerInterface $em): JsonResponse
{
@ -1542,12 +1888,10 @@ class TaxSettingsController extends AbstractController
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$user = $this->getUser();
$userId = $user instanceof \App\Entity\User ? $user->getId() : null;
$repo = $em->getRepository(PluginTaxsettingsKey::class);
$taxSettings = $repo->findOneBy(['business_id' => $businessId, 'user_id' => $userId]);
$taxSettings = $this->getTaxSettings($em, $businessId, $user);
if (!$taxSettings || !$taxSettings->getPrivateKey() || !$taxSettings->getTaxMemoryId()) {
if (!$taxSettings['privateKey'] || !$taxSettings['taxMemoryId']) {
return $this->json([
'success' => false,
'message' => 'تنظیمات مالیاتی تکمیل نشده است. لطفاً ابتدا تنظیمات را تکمیل کنید.'
@ -1583,7 +1927,7 @@ class TaxSettingsController extends AbstractController
}
$buyerInfo = $this->validateBuyerEconomicInfo($invoice);
if (!$buyerInfo['is_valid']) {
return $this->json([
'success' => false,
@ -1611,7 +1955,6 @@ class TaxSettingsController extends AbstractController
$buyerEconomicCode = null;
$missingFields = [];
// دریافت شخص خریدار از ردیف‌های فاکتور
foreach ($invoice->getHesabdariRows() as $row) {
if ($row->getPerson()) {
$buyerPerson = $row->getPerson();
@ -1661,7 +2004,7 @@ class TaxSettingsController extends AbstractController
if (in_array('economic_code', $missingFields)) {
$missingFieldsText[] = 'کد اقتصادی';
}
$result['message'] = 'اطلاعات اقتصادی خریدار ناقص است. فیلدهای زیر تکمیل نشده‌اند: ' . implode('، ', $missingFieldsText);
} else {
$result['message'] = 'اطلاعات اقتصادی خریدار کامل است.';

View file

@ -19,6 +19,7 @@ use App\Entity\StoreroomTicket;
use App\Service\Printers;
use App\Service\registryMGR;
use App\Service\SMS;
use App\Service\PreInvoiceConversionService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@ -385,4 +386,109 @@ class PreinvoiceController extends AbstractController
return $this->json(['id' => $pdfPid]);
}
#[Route('/api/preinvoice/convert-to-invoice/{code}', name: 'app_preinvoice_convert_to_invoice', methods: ['POST'])]
public function convertToInvoice(
string $code,
Request $request,
Access $access,
PreInvoiceConversionService $conversionService,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('plugAccproPresell');
if (!$acc) {
return new JsonResponse($this->extractor->operationFail('دسترسی ندارید'), 403);
}
// پیدا کردن پیش‌فاکتور
$preinvoice = $entityManager->getRepository(PreInvoiceDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
'year' => $acc['year']
]);
if (!$preinvoice) {
return new JsonResponse($this->extractor->operationFail('پیش‌فاکتور یافت نشد'), 404);
}
// بررسی امکان تبدیل
$canConvert = $conversionService->canConvert($preinvoice);
if (!$canConvert['canConvert']) {
return new JsonResponse($this->extractor->operationFail(implode(', ', $canConvert['errors'])), 400);
}
// انجام تبدیل
$result = $conversionService->convertToInvoice($preinvoice, $this->getUser(), $acc);
if ($result['success']) {
return new JsonResponse($result);
} else {
return new JsonResponse($this->extractor->operationFail($result['message']), 500);
}
}
#[Route('/api/preinvoice/can-convert/{code}', name: 'app_preinvoice_can_convert', methods: ['GET'])]
public function canConvertToInvoice(
string $code,
Access $access,
PreInvoiceConversionService $conversionService,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('plugAccproPresell');
if (!$acc) {
return new JsonResponse($this->extractor->operationFail('دسترسی ندارید'), 403);
}
// پیدا کردن پیش‌فاکتور
$preinvoice = $entityManager->getRepository(PreInvoiceDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
'year' => $acc['year']
]);
if (!$preinvoice) {
return new JsonResponse($this->extractor->operationFail('پیش‌فاکتور یافت نشد'), 404);
}
// بررسی امکان تبدیل
$result = $conversionService->canConvert($preinvoice);
return new JsonResponse([
'canConvert' => $result['canConvert'],
'errors' => $result['errors']
]);
}
#[Route('/api/preinvoice/test-values/{code}', name: 'app_preinvoice_test_values', methods: ['GET'])]
public function testPreInvoiceValues(
string $code,
Access $access,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('plugAccproPresell');
if (!$acc) {
return new JsonResponse($this->extractor->operationFail('دسترسی ندارید'), 403);
}
// پیدا کردن پیش‌فاکتور
$preinvoice = $entityManager->getRepository(PreInvoiceDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
'year' => $acc['year']
]);
if (!$preinvoice) {
return new JsonResponse($this->extractor->operationFail('پیش‌فاکتور یافت نشد'), 404);
}
return new JsonResponse([
'totalDiscount' => $preinvoice->getTotalDiscount(),
'totalDiscountPercent' => $preinvoice->getTotalDiscountPercent(),
'shippingCost' => $preinvoice->getShippingCost(),
'showTotalPercentDiscount' => $preinvoice->isShowTotalPercentDiscount(),
'amount' => $preinvoice->getAmount(),
'totalDiscountFloat' => floatval($preinvoice->getTotalDiscount() ?: 0),
'shippingCostFloat' => floatval($preinvoice->getShippingCost() ?: 0)
]);
}
}

View file

@ -53,14 +53,16 @@ class ReportController extends AbstractController
$docs = $entityManagerInterface->getRepository(HesabdariDoc::class)->findBy([
'year' => $acc['year'],
'bid' => $acc['bid'],
'money' => $acc['money']
'money' => $acc['money'],
'isApproved' => true
]);
} else {
$docs = $entityManagerInterface->getRepository(HesabdariDoc::class)->findBy([
'year' => $acc['year'],
'bid' => $acc['bid'],
'type' => $params['type'],
'money' => $acc['money']
'money' => $acc['money'],
'isApproved' => true
]);
}
//filter docs by date
@ -237,14 +239,16 @@ class ReportController extends AbstractController
$docs = $entityManagerInterface->getRepository(HesabdariDoc::class)->findBy([
'year' => $acc['year'],
'bid' => $acc['bid'],
'money' => $acc['money']
'money' => $acc['money'],
'isApproved' => true
]);
} else {
$docs = $entityManagerInterface->getRepository(HesabdariDoc::class)->findBy([
'year' => $acc['year'],
'bid' => $acc['bid'],
'type' => $params['type'],
'money' => $acc['money']
'money' => $acc['money'],
'isApproved' => true
]);
}

View file

@ -46,9 +46,17 @@ class SalaryController extends AbstractController
foreach ($datas as $data) {
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'salary' => $data
]);
// Use query builder to filter by approved documents
$items = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.salary = :salary')
->andWhere('d.isApproved = :isApproved')
->setParameter('salary', $data)
->setParameter('isApproved', true)
->getQuery()
->getResult();
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
@ -73,9 +81,17 @@ class SalaryController extends AbstractController
// محاسبه بدهکار و بستانکار و تراز
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'salary' => $data
]);
// Use query builder to filter by approved documents
$items = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.salary = :salary')
->andWhere('d.isApproved = :isApproved')
->setParameter('salary', $data)
->setParameter('isApproved', true)
->getQuery()
->getResult();
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
@ -141,8 +157,17 @@ class SalaryController extends AbstractController
$salary = $entityManager->getRepository(Salary::class)->findOneBy(['bid' => $acc['bid'], 'code' => $code]);
if (!$salary)
throw $this->createNotFoundException();
//check accounting docs
$rows = $entityManager->getRepository(HesabdariRow::class)->findby(['bid' => $acc['bid'], 'salary' => $salary]);
//check accounting docs - include both approved and preview documents for deletion check
$rows = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.salary = :salary')
->setParameter('bid', $acc['bid'])
->setParameter('salary', $salary)
->getQuery()
->getResult();
if (count($rows) > 0)
return $this->json(['result' => 2]);
@ -187,9 +212,17 @@ class SalaryController extends AbstractController
foreach ($datas as $data) {
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'salary' => $data
]);
// Use query builder to filter by approved documents
$items = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.salary = :salary')
->andWhere('d.isApproved = :isApproved')
->setParameter('salary', $data)
->setParameter('isApproved', true)
->getQuery()
->getResult();
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
@ -221,9 +254,17 @@ class SalaryController extends AbstractController
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'salary' => $salary
]);
// Use query builder to filter by approved documents
$items = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.salary = :salary')
->andWhere('d.isApproved = :isApproved')
->setParameter('salary', $salary)
->setParameter('isApproved', true)
->getQuery()
->getResult();
foreach ($items as $item) {
$bs += $item->getBs();
@ -261,11 +302,20 @@ class SalaryController extends AbstractController
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.salary = :salary')
->andWhere('r.bid = :bid')
->setParameter('salary', $salary)
->setParameter('bid', $acc['bid']);
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
if (isset($params['startDate']) && isset($params['endDate'])) {
$query->andWhere('r.doc.date BETWEEN :startDate AND :endDate')
->setParameter('startDate', $params['startDate'])
@ -303,12 +353,29 @@ class SalaryController extends AbstractController
$salary = $entityManager->getRepository(Salary::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$salary)
throw $this->createNotFoundException();
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'salary' => $salary,
'year' => $acc['year'],
]);
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.salary = :salary')
->andWhere('r.year = :year')
->setParameter('bid', $acc['bid'])
->setParameter('salary', $salary)
->setParameter('year', $acc['year']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$transactions = $query->getQuery()->getResult();
} else {
$transactions = [];
if (is_array($params['items'])) {
@ -322,7 +389,10 @@ class SalaryController extends AbstractController
'year' => $acc['year'],
]);
if ($row) {
$transactions[] = $row;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $row->getDoc()->isApproved()) {
$transactions[] = $row;
}
}
}
}
@ -378,12 +448,29 @@ class SalaryController extends AbstractController
$salary = $entityManager->getRepository(Salary::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$salary)
throw $this->createNotFoundException();
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'salary' => $salary,
'year' => $acc['year'],
]);
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.salary = :salary')
->andWhere('r.year = :year')
->setParameter('bid', $acc['bid'])
->setParameter('salary', $salary)
->setParameter('year', $acc['year']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$transactions = $query->getQuery()->getResult();
} else {
$transactions = [];
if (is_array($params['items'])) {
@ -397,7 +484,10 @@ class SalaryController extends AbstractController
'year' => $acc['year'],
]);
if ($row) {
$transactions[] = $row;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $row->getDoc()->isApproved()) {
$transactions[] = $row;
}
}
}
}

View file

@ -2,6 +2,7 @@
namespace App\Controller;
use App\Entity\Business;
use App\Service\AccountingPermissionService;
use App\Service\Jdate;
use App\Service\Log;
@ -44,7 +45,7 @@ class SellController extends AbstractController
if (!$acc)
throw $this->createAccessDeniedException();
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'bid' => $acc['bid'],
'code' => $code,
'money' => $acc['money']
@ -73,7 +74,7 @@ class SellController extends AbstractController
{
$acc = $access->hasRole('sell');
if (!$acc) throw $this->createAccessDeniedException();
$doc = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findOneBy([
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
'money' => $acc['money']
@ -90,7 +91,7 @@ class SellController extends AbstractController
{
$acc = $access->hasRole('sell');
if (!$acc) throw $this->createAccessDeniedException();
$paymentDoc = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findOneBy([
$paymentDoc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
'money' => $acc['money'],
@ -162,246 +163,235 @@ class SellController extends AbstractController
return $this->json($result);
}
#[Route('/api/sell/mod', name: 'app_sell_mod')]
public function app_sell_mod(
AccountingPermissionService $accountingPermissionService,
PluginService $pluginService,
SMS $SMS,
Provider $provider,
Extractor $extractor,
Request $request,
Access $access,
Log $log,
EntityManagerInterface $entityManager,
registryMGR $registryMGR
): JsonResponse {
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
// #[Route('/api/sell/mod', name: 'app_sell_mod')]
// public function app_sell_mod(
// AccountingPermissionService $accountingPermissionService,
// PluginService $pluginService,
// SMS $SMS,
// Provider $provider,
// Extractor $extractor,
// Request $request,
// Access $access,
// Log $log,
// EntityManagerInterface $entityManager,
// registryMGR $registryMGR
// ): JsonResponse {
// $params = [];
// if ($content = $request->getContent()) {
// $params = json_decode($content, true);
// }
$acc = $access->hasRole('sell');
if (!$acc)
throw $this->createAccessDeniedException();
// $acc = $access->hasRole('sell');
// if (!$acc)
// throw $this->createAccessDeniedException();
$pkgcntr = $accountingPermissionService->canRegisterAccountingDoc($acc['bid']);
if ($pkgcntr['code'] == 4) {
return $this->json([
'result' => 4,
'message' => $pkgcntr['message']
]);
}
if (!array_key_exists('update', $params)) {
return $this->json($extractor->paramsNotSend());
}
if ($params['update'] != '') {
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['update'],
'money' => $acc['money']
]);
if (!$doc)
return $this->json($extractor->notFound());
// $pkgcntr = $accountingPermissionService->canRegisterAccountingDoc($acc['bid']);
// if ($pkgcntr['code'] == 4) {
// return $this->json([
// 'result' => 4,
// 'message' => $pkgcntr['message']
// ]);
// }
// if (!array_key_exists('update', $params)) {
// return $this->json($extractor->paramsNotSend());
// }
// if ($params['update'] != '') {
// $doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
// 'bid' => $acc['bid'],
// 'year' => $acc['year'],
// 'code' => $params['update'],
// 'money' => $acc['money']
// ]);
// if (!$doc)
// return $this->json($extractor->notFound());
// حذف سطرهای قبلی
$rows = $doc->getHesabdariRows();
foreach ($rows as $row)
$entityManager->remove($row);
// // حذف سطرهای قبلی
// $rows = $doc->getHesabdariRows();
// foreach ($rows as $row)
// $entityManager->remove($row);
// حذف سندهای پرداخت قبلی
$relatedDocs = $doc->getRelatedDocs();
foreach ($relatedDocs as $relatedDoc) {
if ($relatedDoc->getType() === 'sell_receive') {
$relatedRows = $relatedDoc->getHesabdariRows();
foreach ($relatedRows as $row) {
$entityManager->remove($row);
}
$entityManager->remove($relatedDoc);
}
}
$entityManager->flush();
} else {
$doc = new HesabdariDoc();
$doc->setBid($acc['bid']);
$doc->setYear($acc['year']);
$doc->setDateSubmit(time());
$doc->setType('sell');
$doc->setSubmitter($this->getUser());
$doc->setMoney($acc['money']);
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
// // حذف سندهای پرداخت قبلی
// $relatedDocs = $doc->getRelatedDocs();
// foreach ($relatedDocs as $relatedDoc) {
// if ($relatedDoc->getType() === 'sell_receive') {
// $relatedRows = $relatedDoc->getHesabdariRows();
// foreach ($relatedRows as $row) {
// $entityManager->remove($row);
// }
// $entityManager->remove($relatedDoc);
// }
// }
// $entityManager->flush();
// } else {
// $doc = new HesabdariDoc();
// $doc->setBid($acc['bid']);
// $doc->setYear($acc['year']);
// $doc->setDateSubmit(time());
// $doc->setType('sell');
// $doc->setSubmitter($this->getUser());
// $doc->setMoney($acc['money']);
// $doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
// Set approval fields based on business settings
$business = $acc['bid'];
if ($business->isRequireTwoStepApproval()) {
$doc->setIsPreview(true);
$doc->setIsApproved(false);
} else {
$doc->setIsPreview(false);
$doc->setIsApproved(true);
}
}
if ($params['transferCost'] != 0) {
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDes('حمل و نقل کالا');
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs($params['transferCost']);
$hesabdariRow->setBd(0);
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
'code' => '61'
]);
$hesabdariRow->setRef($ref);
$entityManager->persist($hesabdariRow);
}
if ($params['discountAll'] != 0) {
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDes('تخفیف فاکتور');
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs(0);
$hesabdariRow->setBd($params['discountAll']);
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
'code' => '104'
]);
$hesabdariRow->setRef($ref);
$entityManager->persist($hesabdariRow);
// // Set approval fields based on business settings
// }
// if ($params['transferCost'] != 0) {
// $hesabdariRow = new HesabdariRow();
// $hesabdariRow->setDes('حمل و نقل کالا');
// $hesabdariRow->setBid($acc['bid']);
// $hesabdariRow->setYear($acc['year']);
// $hesabdariRow->setDoc($doc);
// $hesabdariRow->setBs($params['transferCost']);
// $hesabdariRow->setBd(0);
// $ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
// 'code' => '61'
// ]);
// $hesabdariRow->setRef($ref);
// $entityManager->persist($hesabdariRow);
// }
// if ($params['discountAll'] != 0) {
// $hesabdariRow = new HesabdariRow();
// $hesabdariRow->setDes('تخفیف فاکتور');
// $hesabdariRow->setBid($acc['bid']);
// $hesabdariRow->setYear($acc['year']);
// $hesabdariRow->setDoc($doc);
// $hesabdariRow->setBs(0);
// $hesabdariRow->setBd($params['discountAll']);
// $ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
// 'code' => '104'
// ]);
// $hesabdariRow->setRef($ref);
// $entityManager->persist($hesabdariRow);
// ذخیره نوع تخفیف و درصد آن
$doc->setDiscountType($params['discountType'] ?? 'fixed');
if (isset($params['discountPercent'])) {
$doc->setDiscountPercent((float) $params['discountPercent']);
}
}
$doc->setDes($params['des']);
$doc->setDate($params['date']);
$sumTax = 0;
$sumTotal = 0;
foreach ($params['rows'] as $row) {
$sumTax += $row['tax'];
$sumTotal += $row['sumWithoutTax'];
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDes($row['des']);
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs($row['sumWithoutTax'] + $row['tax']);
$hesabdariRow->setBd(0);
$hesabdariRow->setDiscount($row['discount']);
$hesabdariRow->setTax($row['tax']);
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
'code' => '53'
]);
$hesabdariRow->setRef($ref);
$row['count'] = str_replace(',', '', $row['count']);
$commodity = $entityManager->getRepository(Commodity::class)->findOneBy([
'id' => $row['commodity']['id'],
'bid' => $acc['bid']
]);
if (!$commodity)
return $this->json($extractor->paramsNotSend());
$hesabdariRow->setCommodity($commodity);
$hesabdariRow->setCommdityCount($row['count']);
$entityManager->persist($hesabdariRow);
// // ذخیره نوع تخفیف و درصد آن
// $doc->setDiscountType($params['discountType'] ?? 'fixed');
// if (isset($params['discountPercent'])) {
// $doc->setDiscountPercent((float) $params['discountPercent']);
// }
// }
// $doc->setDes($params['des']);
// $doc->setDate($params['date']);
// $sumTax = 0;
// $sumTotal = 0;
// foreach ($params['rows'] as $row) {
// $sumTax += $row['tax'];
// $sumTotal += $row['sumWithoutTax'];
// $hesabdariRow = new HesabdariRow();
// $hesabdariRow->setDes($row['des']);
// $hesabdariRow->setBid($acc['bid']);
// $hesabdariRow->setYear($acc['year']);
// $hesabdariRow->setDoc($doc);
// $hesabdariRow->setBs($row['sumWithoutTax'] + $row['tax']);
// $hesabdariRow->setBd(0);
// $hesabdariRow->setDiscount($row['discount']);
// $hesabdariRow->setTax($row['tax']);
// $ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
// 'code' => '53'
// ]);
// $hesabdariRow->setRef($ref);
// $row['count'] = str_replace(',', '', $row['count']);
// $commodity = $entityManager->getRepository(Commodity::class)->findOneBy([
// 'id' => $row['commodity']['id'],
// 'bid' => $acc['bid']
// ]);
// if (!$commodity)
// return $this->json($extractor->paramsNotSend());
// $hesabdariRow->setCommodity($commodity);
// $hesabdariRow->setCommdityCount($row['count']);
// $entityManager->persist($hesabdariRow);
if ($acc['bid']->isCommodityUpdateSellPriceAuto() == true && $commodity->getPriceSell() != $row['price']) {
$commodity->setPriceSell($row['price']);
$entityManager->persist($commodity);
}
}
$doc->setAmount($sumTax + $sumTotal - $params['discountAll'] + $params['transferCost']);
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDes('فاکتور فروش');
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs(0);
$hesabdariRow->setBd($sumTax + $sumTotal + $params['transferCost'] - $params['discountAll']);
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
'code' => '3'
]);
$hesabdariRow->setRef($ref);
$person = $entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['person']['code']
]);
if (!$person)
return $this->json($extractor->paramsNotSend());
$hesabdariRow->setPerson($person);
$entityManager->persist($hesabdariRow);
// if ($acc['bid']->isCommodityUpdateSellPriceAuto() == true && $commodity->getPriceSell() != $row['price']) {
// $commodity->setPriceSell($row['price']);
// $entityManager->persist($commodity);
// }
// }
// $doc->setAmount($sumTax + $sumTotal - $params['discountAll'] + $params['transferCost']);
// $hesabdariRow = new HesabdariRow();
// $hesabdariRow->setDes('فاکتور فروش');
// $hesabdariRow->setBid($acc['bid']);
// $hesabdariRow->setYear($acc['year']);
// $hesabdariRow->setDoc($doc);
// $hesabdariRow->setBs(0);
// $hesabdariRow->setBd($sumTax + $sumTotal + $params['transferCost'] - $params['discountAll']);
// $ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
// 'code' => '3'
// ]);
// $hesabdariRow->setRef($ref);
// $person = $entityManager->getRepository(Person::class)->findOneBy([
// 'bid' => $acc['bid'],
// 'code' => $params['person']['code']
// ]);
// if (!$person)
// return $this->json($extractor->paramsNotSend());
// $hesabdariRow->setPerson($person);
// $entityManager->persist($hesabdariRow);
// Two-step approval: اگر کسب‌وکار تأیید دو مرحله‌ای را الزامی کرده باشد
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
if ($businessRequire) {
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
} else {
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
}
$entityManager->persist($doc);
$entityManager->flush();
if (!$doc->getShortlink()) {
$doc->setShortlink($provider->RandomString(8));
}
// if ($TwoStepApproval) {
// $doc->setIsPreview(true);
// $doc->setIsApproved(false);
// $doc->setApprovedBy(null);
// } else {
// $doc->setIsPreview(false);
// $doc->setIsApproved(true);
// $doc->setApprovedBy($this->getUser());
// }
// $entityManager->persist($doc);
// $entityManager->flush();
// if (!$doc->getShortlink()) {
// $doc->setShortlink($provider->RandomString(8));
// }
if (array_key_exists('pair_docs', $params)) {
foreach ($params['pair_docs'] as $pairCode) {
$pair = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $pairCode,
]);
if ($pair) {
$pair->addRelatedDoc($doc);
}
}
}
$entityManager->persist($doc);
$entityManager->flush();
// if (array_key_exists('pair_docs', $params)) {
// foreach ($params['pair_docs'] as $pairCode) {
// $pair = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
// 'bid' => $acc['bid'],
// 'code' => $pairCode,
// ]);
// if ($pair) {
// $pair->addRelatedDoc($doc);
// }
// }
// }
// $entityManager->persist($doc);
// $entityManager->flush();
$log->insert(
'حسابداری',
'سند حسابداری شماره ' . $doc->getCode() . ' ثبت / ویرایش شد.',
$this->getUser(),
$request->headers->get('activeBid'),
$doc
);
if (array_key_exists('sms', $params)) {
if ($params['sms'] == true) {
if ($pluginService->isActive('accpro', $acc['bid']) && $person->getMobile() != '' && $acc['bid']->getTel()) {
return $this->json([
'result' =>
$SMS->sendByBalance(
[$person->getnikename(), 'sell/' . $acc['bid']->getId() . '/' . $doc->getShortlink(), $acc['bid']->getName(), $acc['bid']->getTel()],
$registryMGR->get('sms', 'plugAccproSharefaktor'),
$person->getMobile(),
$acc['bid'],
$this->getUser(),
3
)
]);
} else {
return $this->json([
'result' =>
$SMS->sendByBalance(
[$acc['bid']->getName(), 'sell/' . $acc['bid']->getId() . '/' . $doc->getShortlink()],
$registryMGR->get('sms', 'sharefaktor'),
$person->getMobile(),
$acc['bid'],
$this->getUser(),
3
)
]);
}
}
}
return $this->json($extractor->operationSuccess());
}
// $log->insert(
// 'حسابداری',
// 'سند حسابداری شماره ' . $doc->getCode() . ' ثبت / ویرایش شد.',
// $this->getUser(),
// $request->headers->get('activeBid'),
// $doc
// );
// if (array_key_exists('sms', $params)) {
// if ($params['sms'] == true) {
// if ($pluginService->isActive('accpro', $acc['bid']) && $person->getMobile() != '' && $acc['bid']->getTel()) {
// return $this->json([
// 'result' =>
// $SMS->sendByBalance(
// [$person->getnikename(), 'sell/' . $acc['bid']->getId() . '/' . $doc->getShortlink(), $acc['bid']->getName(), $acc['bid']->getTel()],
// $registryMGR->get('sms', 'plugAccproSharefaktor'),
// $person->getMobile(),
// $acc['bid'],
// $this->getUser(),
// 3
// )
// ]);
// } else {
// return $this->json([
// 'result' =>
// $SMS->sendByBalance(
// [$acc['bid']->getName(), 'sell/' . $acc['bid']->getId() . '/' . $doc->getShortlink()],
// $registryMGR->get('sms', 'sharefaktor'),
// $person->getMobile(),
// $acc['bid'],
// $this->getUser(),
// 3
// )
// ]);
// }
// }
// }
// return $this->json($extractor->operationSuccess());
// }
#[Route('/api/sell/label/change', name: 'app_sell_label_change')]
public function app_sell_label_change(Request $request, Access $access, Extractor $extractor, Log $log, EntityManagerInterface $entityManager): JsonResponse
@ -690,7 +680,7 @@ class SellController extends AbstractController
if ($commodityId) {
$last = $entityManager->getRepository(HesabdariRow::class)
->findOneBy(['commodity' => $commodityId, 'bs' => 0], ['id' => 'DESC']);
if ($last) {
if ($last && $last->getCommdityCount() > 0 && $item->getCommdityCount() > 0) {
$price = $last->getBd() / $last->getCommdityCount();
$profit += ($item->getBs() / $item->getCommdityCount() - $price) * $item->getCommdityCount();
} else {
@ -715,7 +705,7 @@ class SellController extends AbstractController
$avg += $last->getBd();
$count += $last->getCommdityCount();
}
if ($count != 0) {
if ($count != 0 && $item->getCommdityCount() > 0) {
$price = $avg / $count;
$profit += ($item->getBs() / $item->getCommdityCount() - $price) * $item->getCommdityCount();
} else {
@ -847,10 +837,13 @@ class SellController extends AbstractController
$accountStatus['label'] = 'بدهکار';
$accountStatus['value'] = $bd - $bs;
}
// فقط در صورت تایید نهایی مجاز به چاپ هستیم
if ($doc->getStatus() !== 'approved') {
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
$twoApproval = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
if ($twoApproval && $doc->isApproved() !== true && $doc->isPreview() == true) {
return $this->json(['result' => -10, 'message' => 'فاکتور هنوز تایید نشده است'], 403);
}
if ($params['pdf'] == true || $params['printers'] == true) {
$note = '';
if ($printSettings) {
@ -902,6 +895,7 @@ class SellController extends AbstractController
'taxPercent' => method_exists($doc, 'getTaxPercent') ? $doc->getTaxPercent() : null,
'discountPercent' => $doc->getDiscountPercent(),
'discountType' => $doc->getDiscountType(),
'amount' => $doc->getAmount(),
'money' => [
'shortName' => method_exists($doc, 'getMoney') && $doc->getMoney() && method_exists($doc->getMoney(), 'getShortName') ? $doc->getMoney()->getShortName() : null,
],
@ -1062,6 +1056,9 @@ class SellController extends AbstractController
'message' => $pkgcntr['message']
]);
}
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
$TwoStepApproval = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
try {
// بررسی وجود فاکتور برای ویرایش
@ -1104,6 +1101,15 @@ class SellController extends AbstractController
$doc->setSubmitter($this->getUser());
$doc->setMoney($acc['money']);
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
if ($TwoStepApproval) {
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
} else {
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
}
}
// تنظیم اطلاعات اصلی فاکتور
@ -1260,19 +1266,6 @@ class SellController extends AbstractController
$hesabdariRow->setPerson($person);
$entityManager->persist($hesabdariRow);
// Two-step approval: اگر کسب‌وکار تأیید دو مرحله‌ای را الزامی کرده باشد
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
if ($businessRequire) {
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
} else {
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
}
// ذخیره فاکتور
$entityManager->persist($doc);
$entityManager->flush();
@ -1300,6 +1293,16 @@ class SellController extends AbstractController
$paymentDoc->setDes($payment['description'] ?? 'دریافت وجه فاکتور فروش شماره ' . $doc->getCode());
$paymentDoc->setAmount($payment['amount']);
if ($TwoStepApproval) {
$paymentDoc->setIsPreview(true);
$paymentDoc->setIsApproved(false);
$paymentDoc->setApprovedBy(null);
} else {
$paymentDoc->setIsPreview(false);
$paymentDoc->setIsApproved(true);
$paymentDoc->setApprovedBy($this->getUser());
}
// ایجاد ارتباط با فاکتور اصلی
$doc->addRelatedDoc($paymentDoc);
@ -1356,16 +1359,9 @@ class SellController extends AbstractController
$receiveRef = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '3']);
$receiveRow->setRef($receiveRef);
$receiveRow->setPerson($person);
$entityManager->persist($receiveRow);
// Two-step approval برای دریافت/پرداخت
// $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
// $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
// if ($businessRequire) {
// $paymentDoc->setStatus('pending_approval');
// } else {
// $paymentDoc->setStatus('approved');
// }
$entityManager->persist($paymentDoc);
}
$entityManager->flush();
@ -1436,7 +1432,7 @@ class SellController extends AbstractController
throw $this->createAccessDeniedException();
}
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $id,

View file

@ -0,0 +1,575 @@
<?php
namespace App\Controller;
use App\Entity\Business;
use App\Entity\HesabdariDoc;
use App\Entity\HesabdariRow;
use App\Entity\Person;
use App\Entity\Commodity;
use App\Service\Access;
use App\Service\Log;
use App\Service\Jdate;
use App\Service\SellReportService;
use App\Service\PluginService;
use App\Service\Provider;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class SellReportController extends AbstractController
{
#[Route('/api/sell/report/summary', name: 'app_sell_report_summary', methods: ['GET'])]
public function getSellSummary(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
SellReportService $sellReportService,
PluginService $pluginService,
Jdate $jdate
): JsonResponse {
// بررسی دسترسی
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// بررسی فعال بودن پلاگین accpro
if (!$pluginService->isActive('accpro', $acc['bid'])) {
return $this->json([
'result' => 0,
'message' => 'پلاگین accpro فعال نیست'
], 403);
}
// دریافت اطلاعات کسب و کار
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
if (!$business) {
return $this->json([
'result' => 0,
'message' => 'کسب و کار یافت نشد'
], 404);
}
// دریافت پارامترها
$startDate = $request->query->get('startDate');
$endDate = $request->query->get('endDate');
$groupBy = $request->query->get('groupBy', 'day');
$customerId = $request->query->get('customerId');
$status = $request->query->get('status');
// بررسی وضعیت فقط اگر سیستم تایید دو مرحله‌ای فعال باشد
if ($status && !$business->isRequireTwoStepApproval()) {
$status = null;
}
// تنظیم تاریخ‌های پیش‌فرض اگر ارسال نشده باشند
if (!$startDate) {
$startDate = $jdate->jdate('Y/m/01', time()); // ابتدای ماه جاری
}
if (!$endDate) {
$endDate = $jdate->jdate('Y/m/d', time()); // امروز
}
try {
$summary = $sellReportService->getSellSummary(
$acc['bid'],
$acc['year'],
$acc['money'],
$startDate,
$endDate,
$groupBy,
$customerId,
$status
);
return $this->json([
'result' => 1,
'data' => $summary
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => $e->getMessage()
], 500);
}
}
#[Route('/api/sell/report/invoices', name: 'app_sell_report_invoices', methods: ['GET'])]
public function getSellInvoices(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
SellReportService $sellReportService,
PluginService $pluginService
): JsonResponse {
// بررسی دسترسی
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// بررسی فعال بودن پلاگین accpro
if (!$pluginService->isActive('accpro', $acc['bid'])) {
return $this->json([
'result' => 0,
'message' => 'پلاگین accpro فعال نیست'
], 403);
}
// دریافت اطلاعات کسب و کار
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
if (!$business) {
return $this->json([
'result' => 0,
'message' => 'کسب و کار یافت نشد'
], 404);
}
// دریافت پارامترها
$startDate = $request->query->get('startDate');
$endDate = $request->query->get('endDate');
$customerId = $request->query->get('customerId');
$commodityId = $request->query->get('commodityId');
$status = $request->query->get('status');
$page = max(1, (int) $request->query->get('page', 1));
$perPage = max(1, min(100, (int) $request->query->get('perPage', 20)));
// بررسی وضعیت فقط اگر سیستم تایید دو مرحله‌ای فعال باشد
if ($status && !$business->isRequireTwoStepApproval()) {
$status = null;
}
try {
$invoices = $sellReportService->getSellInvoices(
$acc['bid'],
$acc['year'],
$acc['money'],
$startDate,
$endDate,
$customerId,
$commodityId,
$status,
$page,
$perPage
);
return $this->json([
'result' => 1,
'data' => $invoices
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => $e->getMessage()
], 500);
}
}
#[Route('/api/sell/report/top-products', name: 'app_sell_report_top_products', methods: ['GET'])]
public function getTopProducts(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
SellReportService $sellReportService,
PluginService $pluginService
): JsonResponse {
// بررسی دسترسی
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// بررسی فعال بودن پلاگین accpro
if (!$pluginService->isActive('accpro', $acc['bid'])) {
return $this->json([
'result' => 0,
'message' => 'پلاگین accpro فعال نیست'
], 403);
}
// دریافت اطلاعات کسب و کار
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
if (!$business) {
return $this->json([
'result' => 0,
'message' => 'کسب و کار یافت نشد'
], 404);
}
// دریافت پارامترها
$startDate = $request->query->get('startDate');
$endDate = $request->query->get('endDate');
$limit = max(1, min(50, (int) $request->query->get('limit', 10)));
$sortBy = $request->query->get('sortBy', 'amount');
$customerId = $request->query->get('customerId');
$status = $request->query->get('status');
// بررسی وضعیت فقط اگر سیستم تایید دو مرحله‌ای فعال باشد
if ($status && !$business->isRequireTwoStepApproval()) {
$status = null;
}
try {
$topProducts = $sellReportService->getTopProducts(
$acc['bid'],
$acc['year'],
$acc['money'],
$startDate,
$endDate,
$limit,
$sortBy,
$customerId,
$status
);
return $this->json([
'result' => 1,
'data' => $topProducts
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => $e->getMessage()
], 500);
}
}
#[Route('/api/sell/report/top-customers', name: 'app_sell_report_top_customers', methods: ['GET'])]
public function getTopCustomers(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
SellReportService $sellReportService,
PluginService $pluginService
): JsonResponse {
// بررسی دسترسی
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// بررسی فعال بودن پلاگین accpro
if (!$pluginService->isActive('accpro', $acc['bid'])) {
return $this->json([
'result' => 0,
'message' => 'پلاگین accpro فعال نیست'
], 403);
}
// دریافت اطلاعات کسب و کار
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
if (!$business) {
return $this->json([
'result' => 0,
'message' => 'کسب و کار یافت نشد'
], 404);
}
// دریافت پارامترها
$startDate = $request->query->get('startDate');
$endDate = $request->query->get('endDate');
$limit = max(1, min(50, (int) $request->query->get('limit', 10)));
$customerId = $request->query->get('customerId');
$status = $request->query->get('status');
// بررسی وضعیت فقط اگر سیستم تایید دو مرحله‌ای فعال باشد
if ($status && !$business->isRequireTwoStepApproval()) {
$status = null;
}
try {
$topCustomers = $sellReportService->getTopCustomers(
$acc['bid'],
$acc['year'],
$acc['money'],
$startDate,
$endDate,
$limit,
$customerId,
$status
);
return $this->json([
'result' => 1,
'data' => $topCustomers
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => $e->getMessage()
], 500);
}
}
#[Route('/api/sell/report/chart', name: 'app_sell_report_chart', methods: ['GET'])]
public function getSellChart(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
SellReportService $sellReportService,
PluginService $pluginService
): JsonResponse {
// بررسی دسترسی
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// بررسی فعال بودن پلاگین accpro
if (!$pluginService->isActive('accpro', $acc['bid'])) {
return $this->json([
'result' => 0,
'message' => 'پلاگین accpro فعال نیست'
], 403);
}
// دریافت پارامترها
$startDate = $request->query->get('startDate');
$endDate = $request->query->get('endDate');
$groupBy = $request->query->get('groupBy', 'day');
$type = $request->query->get('type', 'amount');
try {
$chartData = $sellReportService->getSellChart(
$acc['bid'],
$acc['year'],
$acc['money'],
$startDate,
$endDate,
$groupBy,
$type
);
return $this->json([
'result' => 1,
'data' => $chartData
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => $e->getMessage()
], 500);
}
}
#[Route('/api/sell/report/customer-analysis', name: 'app_sell_report_customer_analysis', methods: ['GET'])]
public function getCustomerAnalysis(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
SellReportService $sellReportService,
PluginService $pluginService
): JsonResponse {
// بررسی دسترسی
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// بررسی فعال بودن پلاگین accpro
if (!$pluginService->isActive('accpro', $acc['bid'])) {
return $this->json([
'result' => 0,
'message' => 'پلاگین accpro فعال نیست'
], 403);
}
// دریافت پارامترها
$startDate = $request->query->get('startDate');
$endDate = $request->query->get('endDate');
try {
$customerAnalysis = $sellReportService->getCustomerAnalysis(
$acc['bid'],
$acc['year'],
$acc['money'],
$startDate,
$endDate
);
return $this->json([
'result' => 1,
'data' => $customerAnalysis
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => $e->getMessage()
], 500);
}
}
#[Route('/api/sell/report/export', name: 'app_sell_report_export', methods: ['POST'])]
public function exportReport(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
SellReportService $sellReportService,
PluginService $pluginService,
Provider $provider,
Jdate $jdate
): BinaryFileResponse|JsonResponse {
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// بررسی فعال بودن پلاگین accpro
if (!$pluginService->isActive('accpro', $acc['bid'])) {
return $this->json([
'result' => 0,
'message' => 'پلاگین accpro فعال نیست'
], 403);
}
$params = json_decode($request->getContent(), true) ?? [];
$startDate = $params['startDate'] ?? null;
$endDate = $params['endDate'] ?? null;
$customerId = $params['customerId'] ?? null;
$status = $params['status'] ?? null;
try {
// دریافت تمام داده‌های گزارش
$summary = $sellReportService->getSellSummary(
$acc['bid'],
$acc['year'],
$acc['money'],
$startDate,
$endDate,
'day',
$customerId,
$status
);
$topProducts = $sellReportService->getTopProducts(
$acc['bid'],
$acc['year'],
$acc['money'],
$startDate,
$endDate,
100, // تعداد بیشتر برای export
'amount',
$customerId,
$status
);
$topCustomers = $sellReportService->getTopCustomers(
$acc['bid'],
$acc['year'],
$acc['money'],
$startDate,
$endDate,
100, // تعداد بیشتر برای export
$customerId,
$status
);
$invoices = $sellReportService->getSellInvoices(
$acc['bid'],
$acc['year'],
$acc['money'],
$startDate,
$endDate,
$customerId,
null,
$status,
1,
1000 // تعداد بیشتر برای export
);
// آماده‌سازی داده‌ها برای Excel
$excelData = [];
// 1. خلاصه آمار
$excelData[] = ['خلاصه آمار فروش'];
$excelData[] = ['', '']; // خط خالی
$excelData[] = ['کل فروش', number_format($summary['totalAmount']) . ' ریال'];
$excelData[] = ['تعداد فاکتور', number_format($summary['totalCount'])];
$excelData[] = ['میانگین فاکتور', number_format($summary['averageAmount']) . ' ریال'];
$excelData[] = ['کل سود', number_format($summary['totalProfit']) . ' ریال'];
$excelData[] = ['درصد سود', $summary['profitMargin'] . '%'];
$excelData[] = ['بیشترین مبلغ', number_format($summary['maxAmount']) . ' ریال'];
$excelData[] = ['کمترین مبلغ', number_format($summary['minAmount']) . ' ریال'];
$excelData[] = ['', '']; // خط خالی
// 2. محصولات برتر
$excelData[] = ['محصولات پرفروش'];
$excelData[] = ['', '']; // خط خالی
$excelData[] = ['نام کالا', 'کد', 'تعداد', 'مبلغ کل', 'سود', 'درصد سود'];
foreach ($topProducts as $product) {
$excelData[] = [
$product['name'],
$product['code'],
number_format($product['totalCount']),
number_format($product['totalAmount']) . ' ریال',
number_format($product['profit']) . ' ریال',
$product['profitMargin'] . '%'
];
}
$excelData[] = ['', '']; // خط خالی
// 3. مشتریان برتر
$excelData[] = ['مشتریان پرفروش'];
$excelData[] = ['', '']; // خط خالی
$excelData[] = ['نام مشتری', 'کد', 'تعداد فاکتور', 'مبلغ کل', 'میانگین فاکتور'];
foreach ($topCustomers as $customer) {
$excelData[] = [
$customer['name'],
$customer['code'],
number_format($customer['invoiceCount']),
number_format($customer['totalAmount']) . ' ریال',
number_format($customer['averageAmount']) . ' ریال'
];
}
$excelData[] = ['', '']; // خط خالی
// 4. لیست فاکتورها
$excelData[] = ['لیست فاکتورها'];
$excelData[] = ['', '']; // خط خالی
$excelData[] = ['شماره فاکتور', 'تاریخ', 'مشتری', 'مبلغ', 'سود', 'درصد سود', 'وضعیت'];
foreach ($invoices['invoices'] as $invoice) {
$excelData[] = [
$invoice['code'],
$invoice['date'],
$invoice['customer']['name'] ?? '',
number_format($invoice['amount']) . ' ریال',
number_format($invoice['profit']) . ' ریال',
$invoice['profitMargin'] . '%',
$invoice['isApproved'] ? 'تایید شده' : 'پیش‌نمایش'
];
}
// هدرهای Excel
$headers = [
'گزارش فروش - ' . $acc['bid']->getName(),
'تاریخ شروع: ' . ($startDate ?: 'همه'),
'تاریخ پایان: ' . ($endDate ?: 'همه'),
'تاریخ ایجاد: ' . $jdate->jdate('Y/m/d H:i:s', time())
];
// ایجاد فایل Excel
$filePath = $provider->createExcellFromArray($excelData, $headers);
return new BinaryFileResponse($filePath);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => 'خطا در ایجاد گزارش: ' . $e->getMessage()
], 500);
}
}
}

View file

@ -60,16 +60,18 @@ class StoreroomController extends AbstractController
public function uploadTicketAttachment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): JsonResponse
{
$acc = $access->hasRole('store');
if (!$acc) throw $this->createAccessDeniedException();
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid'=>$acc['bid'],'code'=>$code]);
if (!$ticket) throw $this->createNotFoundException('حواله یافت نشد');
if (!$acc)
throw $this->createAccessDeniedException();
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid' => $acc['bid'], 'code' => $code]);
if (!$ticket)
throw $this->createNotFoundException('حواله یافت نشد');
$file = $request->files->get('file');
if (!$file) {
return $this->json(['result'=>-1,'message'=>'فایل ارسال نشده است'], 400);
return $this->json(['result' => -1, 'message' => 'فایل ارسال نشده است'], 400);
}
// Store securely in var/storage
$stored = $storage->store($file, (string)$acc['bid']->getId(), 'storeroom_attachments');
$stored = $storage->store($file, (string) $acc['bid']->getId(), 'storeroom_attachments');
$archive = new ArchiveFile();
$archive->setBid($acc['bid']);
@ -82,33 +84,35 @@ class StoreroomController extends AbstractController
$archive->setDes($request->request->get('des'));
$archive->setRelatedDocType('storeroom_ticket');
$archive->setRelatedDocCode($ticket->getCode());
$archive->setFileSize($stored['size'] !== null ? (string)$stored['size'] : null);
$archive->setFileSize($stored['size'] !== null ? (string) $stored['size'] : null);
$entityManager->persist($archive);
$entityManager->flush();
return $this->json(['result'=>0]);
return $this->json(['result' => 0]);
}
#[Route('/api/storeroom/ticket/attachments/{code}', name: 'app_storeroom_ticket_list_attachments', methods: ['GET'])]
public function listTicketAttachments(string $code, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('store');
if (!$acc) throw $this->createAccessDeniedException();
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid'=>$acc['bid'],'code'=>$code]);
if (!$ticket) throw $this->createNotFoundException('حواله یافت نشد');
if (!$acc)
throw $this->createAccessDeniedException();
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid' => $acc['bid'], 'code' => $code]);
if (!$ticket)
throw $this->createNotFoundException('حواله یافت نشد');
$items = $entityManager->getRepository(ArchiveFile::class)->findBy([
'bid'=>$acc['bid'],
'relatedDocType'=>'storeroom_ticket',
'relatedDocCode'=>$ticket->getCode()
], ['id'=>'DESC']);
return $this->json(array_map(function(ArchiveFile $a){
'bid' => $acc['bid'],
'relatedDocType' => 'storeroom_ticket',
'relatedDocCode' => $ticket->getCode()
], ['id' => 'DESC']);
return $this->json(array_map(function (ArchiveFile $a) {
return [
'id'=>$a->getId(),
'filename'=>$a->getFilename(),
'fileType'=>$a->getFileType(),
'fileSize'=>$a->getFileSize(),
'des'=>$a->getDes(),
'dateSubmit'=>$a->getDateSubmit(),
'id' => $a->getId(),
'filename' => $a->getFilename(),
'fileType' => $a->getFileType(),
'fileSize' => $a->getFileSize(),
'des' => $a->getDes(),
'dateSubmit' => $a->getDateSubmit(),
];
}, $items));
}
@ -117,12 +121,13 @@ class StoreroomController extends AbstractController
public function downloadTicketAttachment(int $id, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): Response
{
$acc = $access->hasRole('store');
if (!$acc) throw $this->createAccessDeniedException();
if (!$acc)
throw $this->createAccessDeniedException();
$a = $entityManager->getRepository(ArchiveFile::class)->find($id);
if (!$a || $a->getBid()->getId() !== $acc['bid']->getId()) {
throw $this->createNotFoundException('فایل یافت نشد');
}
$abs = $storage->absolutePath((string)$a->getFilename());
$abs = $storage->absolutePath((string) $a->getFilename());
if (!is_file($abs) || !is_readable($abs)) {
throw $this->createNotFoundException('فایل موجود نیست');
}
@ -201,7 +206,7 @@ class StoreroomController extends AbstractController
* @throws ReflectionException
*/
#[Route('/api/storeroom/docs/get', name: 'app_storeroom_get_docs')]
public function app_storeroom_get_docs(Provider $provider,Extractor $extractor, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
public function app_storeroom_get_docs(Provider $provider, Extractor $extractor, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('store');
if (!$acc)
@ -238,6 +243,9 @@ class StoreroomController extends AbstractController
]);
$sellsForExport = [];
foreach ($sells as $sell) {
if ($sell->isPreview()) {
continue;
}
$temp = $provider->Entity2Array($sell, 0);
$person = $this->getPerson($sell);
if ($person) {
@ -263,6 +271,9 @@ class StoreroomController extends AbstractController
]);
$rfsellsForExport = [];
foreach ($rfsells as $sell) {
if ($sell->isPreview()) {
continue;
}
$temp = $provider->Entity2Array($sell, 0);
$person = $this->getPerson($sell);
if ($person) {
@ -288,6 +299,9 @@ class StoreroomController extends AbstractController
]);
$rfbuysForExport = [];
foreach ($rfbuys as $buy) {
if ($buy->isPreview()) {
continue;
}
$temp = $provider->Entity2Array($buy, 0);
$person = $this->getPerson($buy);
if ($person) {
@ -457,10 +471,8 @@ class StoreroomController extends AbstractController
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
//check parameters exist
if ((!array_key_exists('ticket', $params)) || (!array_key_exists('items', $params)) || (!array_key_exists('doc', $params)))
$this->createNotFoundException();
//going to save
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'id' => $params['doc']['id'],
'bid' => $acc['bid'],
@ -470,14 +482,11 @@ class StoreroomController extends AbstractController
throw $this->createNotFoundException('سند یافت نشد');
if ($doc->getBid()->getId() != $acc['bid']->getId())
throw $this->createAccessDeniedException('دسترسی به این سند را ندارید.');
//find transfer type
if (!array_key_exists('transferType', $params['ticket']))
throw $this->createNotFoundException('نوع انتقال یافت نشد');
$transferType = $entityManager->getRepository(StoreroomTransferType::class)->find($params['ticket']['transferType']['id']);
if (!$transferType)
throw $this->createNotFoundException('نوع انتقال یافت نشد');
//find storeroom
if (!array_key_exists('store', $params['ticket']))
throw $this->createNotFoundException('انبار یافت نشد');
$storeroom = $entityManager->getRepository(Storeroom::class)->find($params['ticket']['store']['id']);
@ -485,7 +494,6 @@ class StoreroomController extends AbstractController
throw $this->createNotFoundException('انبار یافت نشد');
elseif ($storeroom->getBid()->getId() != $acc['bid']->getId())
throw $this->createAccessDeniedException('دسترسی به این انبار ممکن نیست!');
//find person
if (!array_key_exists('person', $params['ticket']))
throw $this->createNotFoundException('طرف حساب یافت نشد');
$person = $entityManager->getRepository(Person::class)->find($params['ticket']['person']['id']);
@ -493,7 +501,6 @@ class StoreroomController extends AbstractController
throw $this->createNotFoundException('طرف حساب یافت نشد');
elseif ($person->getBid()->getId() != $acc['bid']->getId())
throw $this->createAccessDeniedException('دسترسی به این طرف حساب ممکن نیست!');
$ticket = new StoreroomTicket();
$ticket->setSubmitter($this->getUser());
$ticket->setDate($params['ticket']['date']);
@ -506,7 +513,9 @@ class StoreroomController extends AbstractController
$ticket->setCode($provider->getAccountingCode($acc['bid'], 'storeroom'));
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$rand = '';
for ($i = 0; $i < 8; $i++) { $rand .= $alphabet[random_int(0, strlen($alphabet)-1)]; }
for ($i = 0; $i < 8; $i++) {
$rand .= $alphabet[random_int(0, strlen($alphabet) - 1)];
}
$ticket->setActivationCode($rand);
$ticket->setReceiver($params['ticket']['receiver']);
$ticket->setTransferType($transferType);
@ -520,32 +529,25 @@ class StoreroomController extends AbstractController
$ticket->setImportWorkflowCode($params['ticket']['importWorkflowCode']);
}
$entityManager->persist($ticket);
//$entityManager->flush();
//going to save rows
$docRows = $entityManager->getRepository(HesabdariRow::class)->findBy([
'doc' => $doc
]);
// Determine if warranty serials are required based on flag or provided lines
$hasSerialLines = false;
foreach (($params['items'] ?? []) as $it) {
if (!empty($it['serialLines']) && is_array($it['serialLines'])) { $hasSerialLines = true; break; }
}
$requireWarrantySerial = (isset($params['ticket']['requireWarrantySerial']) && $params['ticket']['requireWarrantySerial'] === true) || $hasSerialLines;
$requireWarrantySerial = (
isset($params['ticket']['requireWarrantySerial'])
&& $params['ticket']['requireWarrantySerial'] === true
&& $pluginService->isActive('warranty', $acc['bid'])
);
if ($requireWarrantySerial) {
if (!$pluginService->isActive('warranty', $acc['bid'])) {
return $this->json(['result' => -5, 'message' => 'افزونه گارانتی فعال نیست'], 403);
}
// Validate counts up-front
foreach ($params['items'] as $item) {
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
if ((int)($item['ticketCount'] ?? 0) > 0 && count($lines) < (int)$item['ticketCount']) {
return $this->json(['result' => -3, 'message' => 'تعداد سریال/گارانتی با تعداد حواله همخوانی ندارد'], 400);
if ((int) ($item['ticketCount'] ?? 0) > 0 && count($lines) < (int) $item['ticketCount']) {
return $this->json([
'result' => -3,
'message' => 'تعداد سریال/گارانتی با تعداد حواله همخوانی ندارد'
], 400);
}
}
}
foreach ($params['items'] as $item) {
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'bid' => $acc['bid'],
@ -556,7 +558,6 @@ class StoreroomController extends AbstractController
throw $this->createNotFoundException('کالا یافت نشد!');
if (!$row->getCommodity())
throw $this->createNotFoundException('کالا یافت نشد!');
//check row count not upper ticket count
if ($row->getCommdityCount() < $item['ticketCount'])
throw $this->createNotFoundException('تعداد کالای اضافه شده بیشتر از تعداد کالا در فاکتور است.');
$ticketItem = new StoreroomItem();
@ -569,21 +570,17 @@ class StoreroomController extends AbstractController
$ticketItem->setCommodity($row->getCommodity());
$ticketItem->setType($item['type']);
$entityManager->persist($ticketItem);
// Bind warranty serials per item if provided
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
if ($requireWarrantySerial) {
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
if ((int)$item['ticketCount'] > 0) {
// Ensure we have an id to bind to
if ((int) $item['ticketCount'] > 0) {
$entityManager->flush();
$lines = array_slice($lines, 0, (int)$item['ticketCount']);
$lines = array_slice($lines, 0, (int) $item['ticketCount']);
foreach ($lines as $ln) {
$warrantyCode = $ln['warranty'] ?? null;
$deviceSerial = $ln['serial'] ?? null;
if (!$warrantyCode) {
return $this->json(['result' => -4, 'message' => 'کد گارانتی ارسال نشده است'], 400);
}
/** @var PlugWarrantySerial|null $serial */
$serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
'business' => $acc['bid'],
'serialNumber' => $warrantyCode,
@ -604,27 +601,51 @@ class StoreroomController extends AbstractController
$entityManager->persist($serial);
}
}
} else {
if (!empty($lines)) {
$entityManager->flush();
foreach ($lines as $ln) {
$warrantyCode = $ln['warranty'] ?? null;
$deviceSerial = $ln['serial'] ?? null;
if (!$warrantyCode) {
continue;
}
$serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
'business' => $acc['bid'],
'serialNumber' => $warrantyCode,
'commodity' => $row->getCommodity(),
]);
if (!$serial || $serial->getStatus() !== PlugWarrantySerial::STATUS_AVAILABLE) {
continue;
}
$serial->setStatus(PlugWarrantySerial::STATUS_CONSUMED);
$serial->setCommoditySerial($deviceSerial);
$serial->setBuyer($person);
$serial->setAllocatedToDocumentId($doc->getId());
$serial->setAllocatedAt(new \DateTimeImmutable());
$serial->setBoundToItemId($ticketItem->getId());
$serial->setBoundAt(new \DateTimeImmutable());
$serial->setActivationTicketCode($ticket->getCode());
$serial->setActivationTicketSecret($ticket->getActivationCode());
$entityManager->persist($serial);
}
}
}
}
$entityManager->flush();
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool) $business->isRequireTwoStepApproval() : false;
if ($businessRequire) {
$ticket->setIsPreview(true);
$ticket->setIsApproved(false);
$ticket->setApprovedBy(null); // هنوز تأیید نشده
$ticket->setApprovedBy(null);
} else {
$ticket->setIsPreview(false);
$ticket->setIsApproved(true);
$ticket->setApprovedBy($this->getUser()); // تأیید شده توسط کاربر فعلی
$ticket->setApprovedBy($this->getUser());
}
//save logs
$log->insert('انبارداری', 'حواله انبار با شماره ' . $ticket->getCode() . ' اضافه / ویرایش شد.', $this->getUser(), $acc['bid']);
if ($pluginService->isActive('accpro', $acc['bid'])) {
//notification to person
if ($params['ticket']['sms'] == true) {
$ticket->setCanShare(true);
$entityManager->persist($ticket);
@ -667,7 +688,6 @@ class StoreroomController extends AbstractController
3
);
}
if ($smsres == 2) {
return $this->json([
'result' => 2
@ -675,7 +695,6 @@ class StoreroomController extends AbstractController
}
}
}
return $this->json([
'result' => 0
]);
@ -689,7 +708,7 @@ class StoreroomController extends AbstractController
throw $this->createAccessDeniedException();
$params = json_decode($request->getContent() ?: '{}', true);
$status = $params['status'] ?? null; // in_progress|done|rejected|approved|pending_approval
if (!in_array($status, ['in_progress','done','rejected','approved','pending_approval'])) {
if (!in_array($status, ['in_progress', 'done', 'rejected', 'approved', 'pending_approval'])) {
return $this->json(['result' => -1, 'message' => 'وضعیت نامعتبر'], 400);
}
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([
@ -720,7 +739,7 @@ class StoreroomController extends AbstractController
$criteria['status'] = $status;
}
$tickets = $entityManager->getRepository(StoreroomTicket::class)->findBy($criteria, ['date' => 'DESC']);
return $this->json(array_map(function(StoreroomTicket $t){
return $this->json(array_map(function (StoreroomTicket $t) {
return [
'code' => $t->getCode(),
'date' => $t->getDate(),
@ -755,7 +774,8 @@ class StoreroomController extends AbstractController
'getDoc',
'getTypeString',
'isPreview',
'isApproved'
'isApproved',
'isCompleted'
], 2);
foreach ($result as $key => &$ticket) {
@ -770,6 +790,16 @@ class StoreroomController extends AbstractController
} else {
$ticket['approvedBy'] = null;
}
if ($ticketEntity->getCompletedBy()) {
$completedBy = $ticketEntity->getCompletedBy();
$ticket['completedBy'] = [
'id' => $completedBy->getId(),
'fullName' => $completedBy->getFullName(),
'email' => $completedBy->getEmail()
];
} else {
$ticket['completedBy'] = null;
}
}
return $this->json($result);
@ -790,7 +820,7 @@ class StoreroomController extends AbstractController
//get items
$items = $entityManager->getRepository(StoreroomItem::class)->findBy(['ticket' => $ticket]);
$res = [];
$res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString', 'isPreview', 'isApproved'], 2);
$res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString', 'isPreview', 'isApproved', 'isCompleted'], 2);
$res['transferType'] = $provider->Entity2ArrayJustIncludes($ticket->getTransferType(), ['getName'], 0);
$res['person'] = $provider->Entity2ArrayJustIncludes($ticket->getPerson(), ['getKeshvar', 'getOstan', 'getShahr', 'getAddress', 'getNikename', 'getCodeeghtesadi', 'getPostalcode', 'getName', 'getTel', 'getSabt'], 0);
//get rows
@ -907,15 +937,15 @@ class StoreroomController extends AbstractController
} else {
$title = 'حواله خروج از انبار';
}
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
if ($businessRequire) {
// بررسی وضعیت تأیید از طریق StoreroomTicket
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool) $business->isRequireTwoStepApproval() : false;
if ($businessRequire && $doc->isApproved() !== true && $doc->isPreview() == true) {
if ($doc->isPreview()) {
return $this->json(['result' => -10, 'message' => 'حواله هنوز تایید نشده است'], 403);
}
}
$pdfPid = 0;
$pdfPid = $provider->createPrint(
$acc['bid'],
@ -964,4 +994,103 @@ class StoreroomController extends AbstractController
);
return $this->json(['id' => $pdfPid]);
}
#[Route('/api/storeroom/ticket/complete/{id}', name: 'app_storeroom_ticket_complete', methods: ['POST'])]
public function app_storeroom_ticket_complete(string $id, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, PluginService $pluginService): JsonResponse
{
$acc = $access->hasRole('store');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([
'code' => $id,
'bid' => $acc['bid']
]);
if (!$ticket)
throw $this->createNotFoundException('حواله یافت نشد');
$requireWarrantySerial = (
isset($params['requireWarrantySerial'])
&& $params['requireWarrantySerial'] === true
&& $pluginService->isActive('warranty', $acc['bid'])
);
if ($pluginService->isActive('warranty', $acc['bid'])) {
$warrantyAllocations = $params['warrantyAllocations'] ?? [];
foreach ($warrantyAllocations as $allocation) {
$commodityId = $allocation['commodityId'] ?? null;
$warrantyLines = $allocation['warrantyLines'] ?? [];
if (!$commodityId || empty($warrantyLines)) {
if ($requireWarrantySerial) {
return $this->json(['result' => -3, 'message' => 'سریال گارانتی برای کالا ارسال نشده است'], 400);
}
continue;
}
$commodity = $entityManager->getRepository(Commodity::class)->find($commodityId);
if (!$commodity) {
if ($requireWarrantySerial) {
return $this->json(['result' => -3, 'message' => 'کالا معتبر نیست برای گارانتی'], 400);
}
continue;
}
foreach ($warrantyLines as $line) {
$warrantySerial = $line['warrantySerial'] ?? null;
$deviceSerial = $line['serialNumber'] ?? null;
$isBeforeAllocated = $line['isBeforeAllocated'] ?? false;
if (!$warrantySerial) {
if ($requireWarrantySerial) {
return $this->json(['result' => -4, 'message' => 'کد گارانتی ارسال نشده است'], 400);
}
continue;
}
if ($isBeforeAllocated) {
continue;
}
$warrantySerialEntity = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
'business' => $acc['bid'],
'serialNumber' => $warrantySerial,
'commodity' => $commodity,
'status' => PlugWarrantySerial::STATUS_AVAILABLE
]);
if (!$warrantySerialEntity) {
if ($requireWarrantySerial) {
return $this->json(['result' => -2, 'message' => "گارانتی {$warrantySerial} یافت نشد یا در دسترس نیست"], 400);
}
continue;
}
$warrantySerialEntity->setStatus(PlugWarrantySerial::STATUS_CONSUMED);
$warrantySerialEntity->setAllocatedToDocumentId($ticket->getId());
$warrantySerialEntity->setActivationTicketCode($ticket->getCode());
$warrantySerialEntity->setActivationTicketSecret($ticket->getActivationCode());
$warrantySerialEntity->setAllocatedToDocumentType('storeroom_ticket');
$warrantySerialEntity->setAllocatedAt(new \DateTimeImmutable());
$warrantySerialEntity->setAllocatedBy($this->getUser());
if ($deviceSerial) {
$warrantySerialEntity->setCommoditySerial($deviceSerial);
}
$entityManager->persist($warrantySerialEntity);
}
}
}
$ticket->setCompleted(true);
$ticket->setCompletedAt(new \DateTimeImmutable());
$ticket->setCompletedBy($this->getUser());
$entityManager->persist($ticket);
$entityManager->flush();
$log->insert('انبارداری', 'پروسه حواله انبار با شماره ' . $ticket->getCode() . ' تکمیل شد.', $this->getUser(), $acc['bid']);
return $this->json([
'result' => 0,
'message' => 'پروسه با موفقیت تکمیل شد'
]);
}
}

View file

@ -19,6 +19,55 @@ final class UpdateCoreController extends AbstractController
$this->connection = $connection;
}
/**
* Helper method to fix Git "dubious ownership" error
*/
private function fixGitOwnershipIssue(string $gitRoot): bool
{
try {
// Check if the directory is a Git repository
if (!is_dir($gitRoot . '/.git')) {
return false;
}
// Always add the directory to safe.directory when this method is called
// This handles cases where Git detects dubious ownership for other reasons
$safeDirProcess = new Process(['git', 'config', '--global', '--add', 'safe.directory', $gitRoot], $gitRoot);
$safeDirProcess->setTimeout(300);
$safeDirProcess->run();
if ($safeDirProcess->isSuccessful()) {
return true;
}
return false;
} catch (\Exception $e) {
return false;
}
}
/**
* Helper method to run Git command with ownership fix
*/
private function runGitCommand(array $command, string $gitRoot, int $timeout = 7200): Process
{
$process = new Process($command, $gitRoot);
$process->setTimeout($timeout);
$process->run();
// If the command failed with "dubious ownership" error, try to fix it
if (!$process->isSuccessful() && str_contains($process->getErrorOutput(), 'dubious ownership')) {
if ($this->fixGitOwnershipIssue($gitRoot)) {
// Retry the command after fixing ownership
$process = new Process($command, $gitRoot);
$process->setTimeout($timeout);
$process->run();
}
}
return $process;
}
#[Route('/api/admin/updatecore/run', name: 'api_admin_updatecore_run', methods: ['POST'])]
public function api_admin_updatecore_run(): JsonResponse
{
@ -42,20 +91,39 @@ final class UpdateCoreController extends AbstractController
'COMPOSER_HOME' => '/var/www/.composer',
]);
// اجرای command به صورت synchronous برای اطمینان از اجرا
$process = new Process(['php', 'hesabixCore/bin/console', 'hesabix:update', $stateFile], $gitRoot, $env);
$process->setTimeout(7200); // افزایش تایم‌اوت به 2 ساعت
$process->start(function ($type, $buffer) use ($stateFile) {
// اجرای command و دریافت خروجی
$process->run(function ($type, $buffer) use ($stateFile) {
$state = json_decode(file_get_contents($stateFile), true) ?? ['uuid' => uniqid(), 'log' => ''];
$state['log'] .= $buffer;
file_put_contents($stateFile, json_encode($state));
});
// بررسی نتیجه اجرا
if (!$process->isSuccessful()) {
$state = json_decode(file_get_contents($stateFile), true) ?? ['uuid' => $uuid, 'log' => ''];
$state['error'] = $process->getErrorOutput();
$state['log'] .= "\nError: " . $process->getErrorOutput();
file_put_contents($stateFile, json_encode($state));
return new JsonResponse([
'status' => 'error',
'message' => 'Update process failed: ' . $process->getErrorOutput(),
'uuid' => $uuid,
], 500);
}
// خواندن وضعیت نهایی
$state = json_decode(file_get_contents($stateFile), true) ?? ['uuid' => $uuid, 'log' => ''];
return new JsonResponse([
'status' => 'started',
'message' => 'Update process started',
'message' => 'Update process completed',
'uuid' => $uuid,
'output' => $state['log'] ?? '',
]);
}
@ -124,7 +192,7 @@ final class UpdateCoreController extends AbstractController
}
#[Route('/api/admin/updatecore/stream', name: 'api_admin_updatecore_stream', methods: ['GET'])]
public function api_admin_updatecore_stream(Request $request): StreamedResponse|JsonResponse
public function api_admin_updatecore_stream(Request $request): JsonResponse
{
$uuid = $request->query->get('uuid');
if (!$uuid) {
@ -135,37 +203,27 @@ final class UpdateCoreController extends AbstractController
$gitRoot = dirname($projectDir);
$stateFile = $gitRoot . '/hesabixBackup/update_state_' . $uuid . '.json';
return new StreamedResponse(function () use ($stateFile) {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
if (!file_exists($stateFile)) {
return new JsonResponse(['status' => 'idle', 'output' => '']);
}
while (true) {
if (!file_exists($stateFile)) {
echo "data: " . json_encode(['status' => 'idle', 'output' => '']) . "\n\n";
ob_flush();
flush();
break;
}
$state = json_decode(file_get_contents($stateFile), true) ?? ['log' => ''];
$output = $state['log'] ?? '';
$state = json_decode(file_get_contents($stateFile), true) ?? ['log' => ''];
$output = $state['log'] ?? '';
$isRunning = !isset($state['error']) &&
!in_array('post_update_test', $state['completedSteps'] ?? []) &&
!str_contains($output, 'No update needed') &&
!str_contains($output, 'Software update completed successfully');
$isRunning = !isset($state['error']) &&
!in_array('post_update_test', $state['completedSteps'] ?? []);
$status = isset($state['error']) ? 'error' : ($isRunning ? 'running' : 'success');
echo "data: " . json_encode(['status' => $status, 'output' => $output]) . "\n\n";
ob_flush();
flush();
if (!$isRunning) {
break;
}
sleep(1);
}
});
$status = isset($state['error']) ? 'error' : ($isRunning ? 'running' : 'success');
return new JsonResponse([
'status' => $status,
'output' => $output,
'completedSteps' => $state['completedSteps'] ?? [],
'error' => $state['error'] ?? null,
'commit_hash' => $state['commit_hash'] ?? null
]);
}
#[Route('/api/admin/updatecore/commits', name: 'api_admin_updatecore_commits', methods: ['GET'])]
@ -174,14 +232,10 @@ final class UpdateCoreController extends AbstractController
$projectDir = $this->getParameter('kernel.project_dir');
$gitRoot = dirname($projectDir); // رفتن به ریشه پروژه
$currentProcess = new Process(['git', 'rev-parse', 'HEAD'], $gitRoot);
$currentProcess->setTimeout(7200); // افزایش تایم‌اوت
$currentProcess->run();
$currentProcess = $this->runGitCommand(['git', 'rev-parse', 'HEAD'], $gitRoot);
$currentCommit = $currentProcess->isSuccessful() ? trim($currentProcess->getOutput()) : 'unknown';
$targetProcess = new Process(['git', 'ls-remote', 'origin', 'HEAD'], $gitRoot);
$targetProcess->setTimeout(7200); // افزایش تایم‌اوت
$targetProcess->run();
$targetProcess = $this->runGitCommand(['git', 'ls-remote', 'origin', 'HEAD'], $gitRoot);
$targetOutput = $targetProcess->isSuccessful() ? explode("\t", trim($targetProcess->getOutput()))[0] : 'unknown';
return new JsonResponse([
@ -453,9 +507,7 @@ final class UpdateCoreController extends AbstractController
}
// دریافت آدرس مخزن origin فعلی
$process = new Process(['git', 'remote', 'get-url', 'origin'], $gitRoot);
$process->setTimeout(7200); // افزایش تایم‌اوت
$process->run();
$process = $this->runGitCommand(['git', 'remote', 'get-url', 'origin'], $gitRoot);
if (!$process->isSuccessful()) {
return new JsonResponse([
@ -483,6 +535,76 @@ final class UpdateCoreController extends AbstractController
}
}
#[Route('/api/admin/updatecore/run-manual', name: 'api_admin_updatecore_run_manual', methods: ['POST'])]
public function api_admin_updatecore_run_manual(Request $request): JsonResponse
{
$uuid = $request->getPayload()->get('uuid');
if (!$uuid) {
return new JsonResponse([
'status' => 'error',
'message' => 'UUID is required',
'output' => '',
], 400);
}
$projectDir = $this->getParameter('kernel.project_dir');
$gitRoot = dirname($projectDir);
$stateFile = $gitRoot . '/hesabixBackup/update_state_' . $uuid . '.json';
if (!file_exists($stateFile)) {
return new JsonResponse([
'status' => 'error',
'message' => 'State file not found',
'output' => '',
], 404);
}
$output = '';
try {
$output .= "شروع اجرای دستی به‌روزرسانی...\n";
// اجرای command hesabix:update
$env = array_merge($_SERVER, [
'HOME' => '/var/www',
'COMPOSER_HOME' => '/var/www/.composer',
]);
$process = new Process(['php', 'hesabixCore/bin/console', 'hesabix:update', $stateFile], $gitRoot, $env);
$process->setTimeout(7200);
$process->run();
$output .= $process->getOutput();
if (!$process->isSuccessful()) {
$output .= "\nخطا: " . $process->getErrorOutput();
return new JsonResponse([
'status' => 'error',
'message' => 'خطا در اجرای به‌روزرسانی',
'output' => $output,
], 500);
}
// بررسی فایل state بعد از اجرا
if (file_exists($stateFile)) {
$state = json_decode(file_get_contents($stateFile), true) ?? [];
$output .= "\nوضعیت نهایی: " . json_encode($state, JSON_PRETTY_PRINT);
}
return new JsonResponse([
'status' => 'success',
'message' => 'به‌روزرسانی با موفقیت انجام شد',
'output' => $output,
]);
} catch (\Exception $e) {
return new JsonResponse([
'status' => 'error',
'message' => 'خطا در اجرای دستی: ' . $e->getMessage(),
'output' => $output,
], 500);
}
}
#[Route('/api/admin/updatecore/change-source', name: 'api_admin_updatecore_change_source', methods: ['POST'])]
public function api_admin_updatecore_change_source(Request $request): JsonResponse
{
@ -513,9 +635,7 @@ final class UpdateCoreController extends AbstractController
$output .= "شروع تغییر آدرس مخزن...\n";
// دریافت آدرس مخزن فعلی
$currentProcess = new Process(['git', 'remote', 'get-url', 'origin'], $gitRoot);
$currentProcess->setTimeout(7200);
$currentProcess->run();
$currentProcess = $this->runGitCommand(['git', 'remote', 'get-url', 'origin'], $gitRoot);
$currentUrl = $currentProcess->isSuccessful() ? trim($currentProcess->getOutput()) : '';
if ($currentUrl) {
@ -523,9 +643,7 @@ final class UpdateCoreController extends AbstractController
}
// تغییر آدرس مخزن origin
$changeProcess = new Process(['git', 'remote', 'set-url', 'origin', $sourceUrl], $gitRoot);
$changeProcess->setTimeout(7200);
$changeProcess->run();
$changeProcess = $this->runGitCommand(['git', 'remote', 'set-url', 'origin', $sourceUrl], $gitRoot);
if (!$changeProcess->isSuccessful()) {
return new JsonResponse([
@ -538,9 +656,7 @@ final class UpdateCoreController extends AbstractController
$output .= "آدرس مخزن به $sourceUrl تغییر یافت\n";
// بررسی اتصال به مخزن جدید
$testProcess = new Process(['git', 'remote', 'show', 'origin'], $gitRoot);
$testProcess->setTimeout(7200);
$testProcess->run();
$testProcess = $this->runGitCommand(['git', 'remote', 'show', 'origin'], $gitRoot);
if (!$testProcess->isSuccessful()) {
return new JsonResponse([
@ -553,9 +669,7 @@ final class UpdateCoreController extends AbstractController
$output .= "اتصال به مخزن جدید با موفقیت برقرار شد\n";
// دریافت اطلاعات مخزن جدید
$fetchProcess = new Process(['git', 'fetch', 'origin'], $gitRoot);
$fetchProcess->setTimeout(7200);
$fetchProcess->run();
$fetchProcess = $this->runGitCommand(['git', 'fetch', 'origin'], $gitRoot);
if (!$fetchProcess->isSuccessful()) {
$output .= "هشدار: خطا در دریافت اطلاعات از مخزن جدید: " . $fetchProcess->getErrorOutput() . "\n";
@ -564,9 +678,7 @@ final class UpdateCoreController extends AbstractController
}
// بررسی branch های موجود
$branchProcess = new Process(['git', 'branch', '-r'], $gitRoot);
$branchProcess->setTimeout(7200);
$branchProcess->run();
$branchProcess = $this->runGitCommand(['git', 'branch', '-r'], $gitRoot);
if ($branchProcess->isSuccessful()) {
$branches = trim($branchProcess->getOutput());
@ -578,9 +690,7 @@ final class UpdateCoreController extends AbstractController
}
// پاک کردن کش Git
$cleanProcess = new Process(['git', 'gc', '--prune=now'], $gitRoot);
$cleanProcess->setTimeout(7200);
$cleanProcess->run();
$cleanProcess = $this->runGitCommand(['git', 'gc', '--prune=now'], $gitRoot);
if ($cleanProcess->isSuccessful()) {
$output .= "کش Git پاک شد\n";

View file

@ -316,14 +316,48 @@ class Business
#[ORM\Column(nullable: true)]
private ?bool $requireTwoStepApproval = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $invoiceApprover = null;
// Two-step approval extended configuration
#[ORM\Column(nullable: true)]
private ?bool $approvalUseSameApprover = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $warehouseApprover = null;
private ?string $approverAll = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $financialApprover = null;
private ?string $approverSellInvoice = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverBuyInvoice = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverReturnBuy = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverReturnSell = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverWarehouseTransfer = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverReceiveFromPersons = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverPayToPersons = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverAccountingDocs = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverBankTransfers = null;
#[ORM\Column(nullable: true)]
private ?bool $requireWarrantyOnDelivery = null;
#[ORM\Column(nullable: true)]
private ?int $activationGraceDays = null;
#[ORM\Column(nullable: true)]
private ?bool $matchWarrantyToSerial = null;
public function __construct()
{
@ -2222,36 +2256,135 @@ class Business
return $this;
}
public function getInvoiceApprover(): ?string
public function getApproverSellInvoice(): ?string
{
return $this->invoiceApprover;
return $this->approverSellInvoice;
}
public function setInvoiceApprover(?string $invoiceApprover): static
public function setApproverSellInvoice(?string $approverSellInvoice): static
{
$this->invoiceApprover = $invoiceApprover;
$this->approverSellInvoice = $approverSellInvoice;
return $this;
}
public function getWarehouseApprover(): ?string
public function getApproverBuyInvoice(): ?string
{
return $this->warehouseApprover;
return $this->approverBuyInvoice;
}
public function setWarehouseApprover(?string $warehouseApprover): static
public function setApproverBuyInvoice(?string $approverBuyInvoice): static
{
$this->warehouseApprover = $warehouseApprover;
$this->approverBuyInvoice = $approverBuyInvoice;
return $this;
}
public function getFinancialApprover(): ?string
public function getApproverReturnBuy(): ?string
{
return $this->financialApprover;
return $this->approverReturnBuy;
}
public function setFinancialApprover(?string $financialApprover): static
public function setApproverReturnBuy(?string $approverReturnBuy): static
{
$this->financialApprover = $financialApprover;
$this->approverReturnBuy = $approverReturnBuy;
return $this;
}
}
public function getApproverReturnSell(): ?string
{
return $this->approverReturnSell;
}
public function setApproverReturnSell(?string $approverReturnSell): static
{
$this->approverReturnSell = $approverReturnSell;
return $this;
}
public function getApproverWarehouseTransfer(): ?string
{
return $this->approverWarehouseTransfer;
}
public function setApproverWarehouseTransfer(?string $approverWarehouseTransfer): static
{
$this->approverWarehouseTransfer = $approverWarehouseTransfer;
return $this;
}
public function getApproverReceiveFromPersons(): ?string
{
return $this->approverReceiveFromPersons;
}
public function setApproverReceiveFromPersons(?string $approverReceiveFromPersons): static
{
$this->approverReceiveFromPersons = $approverReceiveFromPersons;
return $this;
}
public function getApproverPayToPersons(): ?string
{
return $this->approverPayToPersons;
}
public function setApproverPayToPersons(?string $approverPayToPersons): static
{
$this->approverPayToPersons = $approverPayToPersons;
return $this;
}
public function getApproverAccountingDocs(): ?string
{
return $this->approverAccountingDocs;
}
public function setApproverAccountingDocs(?string $approverAccountingDocs): static
{
$this->approverAccountingDocs = $approverAccountingDocs;
return $this;
}
public function getApproverBankTransfers(): ?string
{
return $this->approverBankTransfers;
}
public function setApproverBankTransfers(?string $approverBankTransfers): static
{
$this->approverBankTransfers = $approverBankTransfers;
return $this;
}
public function getRequireWarrantyOnDelivery(): ?bool
{
return $this->requireWarrantyOnDelivery;
}
public function setRequireWarrantyOnDelivery(?bool $requireWarrantyOnDelivery): static
{
$this->requireWarrantyOnDelivery = $requireWarrantyOnDelivery;
return $this;
}
public function getActivationGraceDays(): ?int
{
return $this->activationGraceDays;
}
public function setActivationGraceDays(?int $activationGraceDays): static
{
$this->activationGraceDays = $activationGraceDays;
return $this;
}
public function getMatchWarrantyToSerial(): ?bool
{
return $this->matchWarrantyToSerial;
}
public function setMatchWarrantyToSerial(?bool $matchWarrantyToSerial): static
{
$this->matchWarrantyToSerial = $matchWarrantyToSerial;
return $this;
}
}

View file

@ -66,6 +66,21 @@ class DashboardSettings
#[ORM\Column(nullable: true)]
private ?bool $topIncomesChart = null;
#[ORM\Column(nullable: true)]
private ?bool $cheques = null;
#[ORM\Column(nullable: true)]
private ?bool $chequesDueToday = null;
#[ORM\Column(nullable: true)]
private ?bool $chequesStatusChart = null;
#[ORM\Column(nullable: true)]
private ?bool $chequesMonthlyChart = null;
#[ORM\Column(nullable: true)]
private ?bool $chequesDueSoon = null;
public function getId(): ?int
{
return $this->id;
@ -274,4 +289,64 @@ class DashboardSettings
return $this;
}
public function isCheques(): ?bool
{
return $this->cheques;
}
public function setCheques(?bool $cheques): static
{
$this->cheques = $cheques;
return $this;
}
public function isChequesDueToday(): ?bool
{
return $this->chequesDueToday;
}
public function setChequesDueToday(?bool $chequesDueToday): static
{
$this->chequesDueToday = $chequesDueToday;
return $this;
}
public function isChequesStatusChart(): ?bool
{
return $this->chequesStatusChart;
}
public function setChequesStatusChart(?bool $chequesStatusChart): static
{
$this->chequesStatusChart = $chequesStatusChart;
return $this;
}
public function isChequesMonthlyChart(): ?bool
{
return $this->chequesMonthlyChart;
}
public function setChequesMonthlyChart(?bool $chequesMonthlyChart): static
{
$this->chequesMonthlyChart = $chequesMonthlyChart;
return $this;
}
public function isChequesDueSoon(): ?bool
{
return $this->chequesDueSoon;
}
public function setChequesDueSoon(?bool $chequesDueSoon): static
{
$this->chequesDueSoon = $chequesDueSoon;
return $this;
}
}

View file

@ -728,11 +728,11 @@ class HesabdariDoc
}
// Approval fields
#[ORM\Column(nullable: true)]
private ?bool $isPreview = null;
#[ORM\Column(nullable: true, options: ['default' => false])]
private ?bool $isPreview = false;
#[ORM\Column(nullable: true)]
private ?bool $isApproved = null;
#[ORM\Column(nullable: true, options: ['default' => true])]
private ?bool $isApproved = true;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true)]

View file

@ -368,6 +368,4 @@ class HesabdariRow
return $this;
}
}

View file

@ -60,18 +60,12 @@ class ImportWorkflow
#[ORM\Column(length: 255, nullable: true)]
private ?string $supplierEmail = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $totalAmount = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $currency = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $exchangeRate = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $totalAmountIRR = null;
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowItem::class, orphanRemoval: true)]
private Collection $items;
@ -250,17 +244,6 @@ class ImportWorkflow
return $this;
}
public function getTotalAmount(): ?string
{
return $this->totalAmount;
}
public function setTotalAmount(?string $totalAmount): static
{
$this->totalAmount = $totalAmount;
return $this;
}
public function getCurrency(): ?string
{
return $this->currency;
@ -283,17 +266,6 @@ class ImportWorkflow
return $this;
}
public function getTotalAmountIRR(): ?string
{
return $this->totalAmountIRR;
}
public function setTotalAmountIRR(?string $totalAmountIRR): static
{
$this->totalAmountIRR = $totalAmountIRR;
return $this;
}
public function getItems(): Collection
{
return $this->items;
@ -437,5 +409,15 @@ class ImportWorkflow
}
return $this;
}
public function getComputedTotalAmount(): ?string
{
$items = $this->getItems();
$total = 0;
foreach ($items as $item) {
$total += $item->getTotalPrice();
}
return $total;
}
}

View file

@ -29,16 +29,16 @@ class ImportWorkflowCustoms
#[ORM\Column(length: 255, nullable: true)]
private ?string $clearanceDate = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $customsDuty = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $valueAddedTax = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $otherCharges = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $totalCustomsCharges = null;
#[ORM\Column(length: 255, nullable: true)]

View file

@ -43,16 +43,16 @@ class ImportWorkflowItem
#[ORM\Column(length: 255)]
private ?string $quantity = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $unitPrice = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $unitPriceIRR = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $totalPrice = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $totalPriceIRR = null;
#[ORM\Column(length: 255, nullable: true)]

View file

@ -23,13 +23,13 @@ class ImportWorkflowPayment
#[ORM\Column(length: 255)]
private ?string $type = null;
#[ORM\Column(length: 255)]
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2)]
private ?string $amount = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $currency = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $amountIRR = null;
#[ORM\Column(length: 255)]

View file

@ -0,0 +1,216 @@
<?php
namespace App\Entity;
use App\Repository\OAuthAccessTokenRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: OAuthAccessTokenRepository::class)]
class OAuthAccessToken
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, unique: true)]
private ?string $token = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $refreshToken = null;
#[ORM\Column(type: 'json')]
private array $scopes = [];
#[ORM\Column(type: 'datetime')]
private ?\DateTimeInterface $expiresAt = null;
#[ORM\Column(type: 'datetime')]
private ?\DateTimeInterface $createdAt = null;
#[ORM\Column(type: 'datetime', nullable: true)]
private ?\DateTimeInterface $lastUsedAt = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?OAuthApplication $application = null;
#[ORM\ManyToOne]
private ?OAuthScope $scope = null;
#[ORM\Column(type: 'boolean')]
private bool $isRevoked = false;
#[ORM\Column(length: 255, nullable: true)]
private ?string $ipAddress = null;
#[ORM\Column(length: 500, nullable: true)]
private ?string $userAgent = null;
public function __construct()
{
$this->createdAt = new \DateTime();
$this->lastUsedAt = new \DateTime();
}
public function getId(): ?int
{
return $this->id;
}
public function getToken(): ?string
{
return $this->token;
}
public function setToken(string $token): static
{
$this->token = $token;
return $this;
}
public function getRefreshToken(): ?string
{
return $this->refreshToken;
}
public function setRefreshToken(?string $refreshToken): static
{
$this->refreshToken = $refreshToken;
return $this;
}
public function getScopes(): array
{
return $this->scopes;
}
public function setScopes(array $scopes): static
{
$this->scopes = $scopes;
return $this;
}
public function getExpiresAt(): ?\DateTimeInterface
{
return $this->expiresAt;
}
public function setExpiresAt(\DateTimeInterface $expiresAt): static
{
$this->expiresAt = $expiresAt;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getLastUsedAt(): ?\DateTimeInterface
{
return $this->lastUsedAt;
}
public function setLastUsedAt(?\DateTimeInterface $lastUsedAt): static
{
$this->lastUsedAt = $lastUsedAt;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): static
{
$this->user = $user;
return $this;
}
public function getApplication(): ?OAuthApplication
{
return $this->application;
}
public function setApplication(?OAuthApplication $application): static
{
$this->application = $application;
return $this;
}
public function getScope(): ?OAuthScope
{
return $this->scope;
}
public function setScope(?OAuthScope $scope): static
{
$this->scope = $scope;
return $this;
}
public function isRevoked(): bool
{
return $this->isRevoked;
}
public function setIsRevoked(bool $isRevoked): static
{
$this->isRevoked = $isRevoked;
return $this;
}
public function getIpAddress(): ?string
{
return $this->ipAddress;
}
public function setIpAddress(?string $ipAddress): static
{
$this->ipAddress = $ipAddress;
return $this;
}
public function getUserAgent(): ?string
{
return $this->userAgent;
}
public function setUserAgent(?string $userAgent): static
{
$this->userAgent = $userAgent;
return $this;
}
public function isExpired(): bool
{
return $this->expiresAt < new \DateTime();
}
public function isValid(): bool
{
return !$this->isRevoked && !$this->isExpired();
}
public function hasScope(string $scope): bool
{
return in_array($scope, $this->scopes);
}
public function updateLastUsed(): void
{
$this->lastUsedAt = new \DateTime();
}
}

View file

@ -0,0 +1,332 @@
<?php
namespace App\Entity;
use App\Repository\OAuthApplicationRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\SerializedName;
#[ORM\Entity(repositoryClass: OAuthApplicationRepository::class)]
class OAuthApplication
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
#[Assert\Length(min: 3, max: 255)]
private ?string $name = null;
#[ORM\Column(length: 500, nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
#[Assert\Url]
private ?string $website = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
#[Assert\Url]
private ?string $redirectUri = null;
#[ORM\Column(length: 64, unique: true)]
private ?string $clientId = null;
#[ORM\Column(length: 128)]
private ?string $clientSecret = null;
#[ORM\Column(type: 'boolean')]
#[SerializedName('isActive')]
private bool $isActive = true;
#[ORM\Column(length: 255, nullable: true)]
private ?string $logoUrl = null;
#[ORM\Column(type: 'datetime')]
private ?\DateTimeInterface $createdAt = null;
#[ORM\Column(type: 'datetime')]
private ?\DateTimeInterface $updatedAt = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?User $owner = null;
#[ORM\OneToMany(mappedBy: 'application', targetEntity: OAuthAccessToken::class, orphanRemoval: true)]
private Collection $accessTokens;
#[ORM\OneToMany(mappedBy: 'application', targetEntity: OAuthAuthorizationCode::class, orphanRemoval: true)]
private Collection $authorizationCodes;
#[ORM\ManyToMany(targetEntity: OAuthScope::class, inversedBy: 'applications')]
private Collection $scopes;
#[ORM\Column(type: 'json', nullable: true)]
private array $allowedScopes = [];
#[ORM\Column(type: 'integer', options: ['default' => 0])]
private int $rateLimit = 1000; // تعداد درخواست در ساعت
#[ORM\Column(type: 'json', nullable: true)]
private array $ipWhitelist = [];
public function __construct()
{
$this->accessTokens = new ArrayCollection();
$this->authorizationCodes = new ArrayCollection();
$this->scopes = new ArrayCollection();
$this->createdAt = new \DateTime();
$this->updatedAt = new \DateTime();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getWebsite(): ?string
{
return $this->website;
}
public function setWebsite(string $website): static
{
$this->website = $website;
return $this;
}
public function getRedirectUri(): ?string
{
return $this->redirectUri;
}
public function setRedirectUri(string $redirectUri): static
{
$this->redirectUri = $redirectUri;
return $this;
}
public function getClientId(): ?string
{
return $this->clientId;
}
public function setClientId(string $clientId): static
{
$this->clientId = $clientId;
return $this;
}
public function getClientSecret(): ?string
{
return $this->clientSecret;
}
public function setClientSecret(string $clientSecret): static
{
$this->clientSecret = $clientSecret;
return $this;
}
public function isActive(): bool
{
return $this->isActive;
}
public function setIsActive(bool $isActive): static
{
$this->isActive = $isActive;
return $this;
}
public function getLogoUrl(): ?string
{
return $this->logoUrl;
}
public function setLogoUrl(?string $logoUrl): static
{
$this->logoUrl = $logoUrl;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(\DateTimeInterface $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
public function getOwner(): ?User
{
return $this->owner;
}
public function setOwner(?User $owner): static
{
$this->owner = $owner;
return $this;
}
/**
* @return Collection<int, OAuthAccessToken>
*/
public function getAccessTokens(): Collection
{
return $this->accessTokens;
}
public function addAccessToken(OAuthAccessToken $accessToken): static
{
if (!$this->accessTokens->contains($accessToken)) {
$this->accessTokens->add($accessToken);
$accessToken->setApplication($this);
}
return $this;
}
public function removeAccessToken(OAuthAccessToken $accessToken): static
{
if ($this->accessTokens->removeElement($accessToken)) {
if ($accessToken->getApplication() === $this) {
$accessToken->setApplication(null);
}
}
return $this;
}
/**
* @return Collection<int, OAuthAuthorizationCode>
*/
public function getAuthorizationCodes(): Collection
{
return $this->authorizationCodes;
}
public function addAuthorizationCode(OAuthAuthorizationCode $authorizationCode): static
{
if (!$this->authorizationCodes->contains($authorizationCode)) {
$this->authorizationCodes->add($authorizationCode);
$authorizationCode->setApplication($this);
}
return $this;
}
public function removeAuthorizationCode(OAuthAuthorizationCode $authorizationCode): static
{
if ($this->authorizationCodes->removeElement($authorizationCode)) {
if ($authorizationCode->getApplication() === $this) {
$authorizationCode->setApplication(null);
}
}
return $this;
}
/**
* @return Collection<int, OAuthScope>
*/
public function getScopes(): Collection
{
return $this->scopes;
}
public function addScope(OAuthScope $scope): static
{
if (!$this->scopes->contains($scope)) {
$this->scopes->add($scope);
}
return $this;
}
public function removeScope(OAuthScope $scope): static
{
$this->scopes->removeElement($scope);
return $this;
}
public function getAllowedScopes(): array
{
return $this->allowedScopes;
}
public function setAllowedScopes(array $allowedScopes): static
{
$this->allowedScopes = $allowedScopes;
return $this;
}
public function getRateLimit(): int
{
return $this->rateLimit;
}
public function setRateLimit(int $rateLimit): static
{
$this->rateLimit = $rateLimit;
return $this;
}
public function getIpWhitelist(): array
{
return $this->ipWhitelist;
}
public function setIpWhitelist(array $ipWhitelist): static
{
$this->ipWhitelist = $ipWhitelist;
return $this;
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTime();
}
}

View file

@ -0,0 +1,192 @@
<?php
namespace App\Entity;
use App\Repository\OAuthAuthorizationCodeRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: OAuthAuthorizationCodeRepository::class)]
class OAuthAuthorizationCode
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, unique: true)]
private ?string $code = null;
#[ORM\Column(length: 255)]
private ?string $redirectUri = null;
#[ORM\Column(type: 'json')]
private array $scopes = [];
#[ORM\Column(type: 'datetime')]
private ?\DateTimeInterface $expiresAt = null;
#[ORM\Column(type: 'boolean')]
private bool $isUsed = false;
#[ORM\Column(type: 'datetime')]
private ?\DateTimeInterface $createdAt = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?OAuthApplication $application = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $state = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $codeChallenge = null;
#[ORM\Column(length: 10, nullable: true)]
private ?string $codeChallengeMethod = null;
public function __construct()
{
$this->createdAt = new \DateTime();
$this->expiresAt = (new \DateTime())->modify('+10 minutes');
}
public function getId(): ?int
{
return $this->id;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(string $code): static
{
$this->code = $code;
return $this;
}
public function getRedirectUri(): ?string
{
return $this->redirectUri;
}
public function setRedirectUri(string $redirectUri): static
{
$this->redirectUri = $redirectUri;
return $this;
}
public function getScopes(): array
{
return $this->scopes;
}
public function setScopes(array $scopes): static
{
$this->scopes = $scopes;
return $this;
}
public function getExpiresAt(): ?\DateTimeInterface
{
return $this->expiresAt;
}
public function setExpiresAt(\DateTimeInterface $expiresAt): static
{
$this->expiresAt = $expiresAt;
return $this;
}
public function isUsed(): bool
{
return $this->isUsed;
}
public function setIsUsed(bool $isUsed): static
{
$this->isUsed = $isUsed;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): static
{
$this->user = $user;
return $this;
}
public function getApplication(): ?OAuthApplication
{
return $this->application;
}
public function setApplication(?OAuthApplication $application): static
{
$this->application = $application;
return $this;
}
public function getState(): ?string
{
return $this->state;
}
public function setState(?string $state): static
{
$this->state = $state;
return $this;
}
public function getCodeChallenge(): ?string
{
return $this->codeChallenge;
}
public function setCodeChallenge(?string $codeChallenge): static
{
$this->codeChallenge = $codeChallenge;
return $this;
}
public function getCodeChallengeMethod(): ?string
{
return $this->codeChallengeMethod;
}
public function setCodeChallengeMethod(?string $codeChallengeMethod): static
{
$this->codeChallengeMethod = $codeChallengeMethod;
return $this;
}
public function isExpired(): bool
{
return $this->expiresAt < new \DateTime();
}
public function isValid(): bool
{
return !$this->isUsed && !$this->isExpired();
}
}

View file

@ -0,0 +1,162 @@
<?php
namespace App\Entity;
use App\Repository\OAuthScopeRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: OAuthScopeRepository::class)]
class OAuthScope
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 100, unique: true)]
#[Assert\NotBlank]
#[Assert\Length(min: 3, max: 100)]
#[Assert\Regex('/^[a-z_]+$/')]
private ?string $name = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
private ?string $description = null;
#[ORM\Column(type: 'boolean')]
private bool $isDefault = false;
#[ORM\Column(type: 'boolean')]
private bool $isSystem = false;
#[ORM\Column(type: 'datetime')]
private ?\DateTimeInterface $createdAt = null;
#[ORM\ManyToMany(targetEntity: OAuthApplication::class, mappedBy: 'scopes')]
private Collection $applications;
#[ORM\OneToMany(mappedBy: 'scope', targetEntity: OAuthAccessToken::class)]
private Collection $accessTokens;
public function __construct()
{
$this->applications = new ArrayCollection();
$this->accessTokens = new ArrayCollection();
$this->createdAt = new \DateTime();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(string $description): static
{
$this->description = $description;
return $this;
}
public function isDefault(): bool
{
return $this->isDefault;
}
public function setIsDefault(bool $isDefault): static
{
$this->isDefault = $isDefault;
return $this;
}
public function isSystem(): bool
{
return $this->isSystem;
}
public function setIsSystem(bool $isSystem): static
{
$this->isSystem = $isSystem;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
/**
* @return Collection<int, OAuthApplication>
*/
public function getApplications(): Collection
{
return $this->applications;
}
public function addApplication(OAuthApplication $application): static
{
if (!$this->applications->contains($application)) {
$this->applications->add($application);
$application->addScope($this);
}
return $this;
}
public function removeApplication(OAuthApplication $application): static
{
if ($this->applications->removeElement($application)) {
$application->removeScope($this);
}
return $this;
}
/**
* @return Collection<int, OAuthAccessToken>
*/
public function getAccessTokens(): Collection
{
return $this->accessTokens;
}
public function addAccessToken(OAuthAccessToken $accessToken): static
{
if (!$this->accessTokens->contains($accessToken)) {
$this->accessTokens->add($accessToken);
$accessToken->setScope($this);
}
return $this;
}
public function removeAccessToken(OAuthAccessToken $accessToken): static
{
if ($this->accessTokens->removeElement($accessToken)) {
if ($accessToken->getScope() === $this) {
$accessToken->setScope(null);
}
}
return $this;
}
}

View file

@ -142,11 +142,14 @@ class Permission
private ?bool $ai = null;
#[ORM\Column(nullable: true)]
private ?bool $warehouseManager = null;
private ?bool $storehelper = null;
#[ORM\Column(nullable: true)]
private ?bool $importWorkflow = null;
#[ORM\Column(nullable: true)]
private ?bool $plugHrmAttendance = null;
public function getId(): ?int
{
return $this->id;
@ -656,14 +659,14 @@ class Permission
return $this;
}
public function isWarehouseManager(): ?bool
public function isStorehelper(): ?bool
{
return $this->warehouseManager;
return $this->storehelper;
}
public function setWarehouseManager(?bool $warehouseManager): static
public function setStorehelper(?bool $storehelper): static
{
$this->warehouseManager = $warehouseManager;
$this->storehelper = $storehelper;
return $this;
}
@ -679,4 +682,16 @@ class Permission
return $this;
}
public function isPlugHrmAttendance(): ?bool
{
return $this->plugHrmAttendance;
}
public function setPlugHrmAttendance(?bool $plugHrmAttendance): static
{
$this->plugHrmAttendance = $plugHrmAttendance;
return $this;
}
}

View file

@ -161,6 +161,8 @@ class Person
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $tags = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $paymentId = null;
public function __construct()
@ -916,5 +918,16 @@ class Person
return $this;
}
public function getPaymentId(): ?string
{
return $this->paymentId;
}
public function setPaymentId(?string $paymentId): self
{
$this->paymentId = $paymentId;
return $this;
}
}

View file

@ -0,0 +1,177 @@
<?php
namespace App\Entity;
use App\Repository\PlugHrmAttendanceRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: PlugHrmAttendanceRepository::class)]
class PlugHrmAttendance
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'plugHrmAttendances')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Business $business = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Person $person = null;
#[ORM\Column(length: 10)]
private ?string $date = null; // تاریخ شمسی YYYY/MM/DD
#[ORM\Column(type: Types::INTEGER, nullable: true)]
private ?int $totalHours = 0; // ساعات کل کار (به دقیقه)
#[ORM\Column(type: Types::INTEGER, nullable: true)]
private ?int $overtimeHours = 0; // ساعات اضافه‌کاری (به دقیقه)
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
private ?\DateTimeInterface $createdAt = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
private ?\DateTimeInterface $updatedAt = null;
#[ORM\OneToMany(mappedBy: 'attendance', targetEntity: PlugHrmAttendanceItem::class, orphanRemoval: true)]
private Collection $items;
public function __construct()
{
$this->items = new ArrayCollection();
$this->createdAt = new \DateTime();
$this->updatedAt = new \DateTime();
}
public function getId(): ?int
{
return $this->id;
}
public function getBusiness(): ?Business
{
return $this->business;
}
public function setBusiness(?Business $business): static
{
$this->business = $business;
return $this;
}
public function getPerson(): ?Person
{
return $this->person;
}
public function setPerson(?Person $person): static
{
$this->person = $person;
return $this;
}
public function getDate(): ?string
{
return $this->date;
}
public function setDate(string $date): static
{
$this->date = $date;
return $this;
}
public function getTotalHours(): ?int
{
return $this->totalHours;
}
public function setTotalHours(?int $totalHours): static
{
$this->totalHours = $totalHours;
return $this;
}
public function getOvertimeHours(): ?int
{
return $this->overtimeHours;
}
public function setOvertimeHours(?int $overtimeHours): static
{
$this->overtimeHours = $overtimeHours;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(\DateTimeInterface $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* @return Collection<int, PlugHrmAttendanceItem>
*/
public function getItems(): Collection
{
return $this->items;
}
public function addItem(PlugHrmAttendanceItem $item): static
{
if (!$this->items->contains($item)) {
$this->items->add($item);
$item->setAttendance($this);
}
return $this;
}
public function removeItem(PlugHrmAttendanceItem $item): static
{
if ($this->items->removeElement($item)) {
if ($item->getAttendance() === $this) {
$item->setAttendance(null);
}
}
return $this;
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace App\Entity;
use App\Repository\PlugHrmAttendanceItemRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: PlugHrmAttendanceItemRepository::class)]
class PlugHrmAttendanceItem
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'items')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?PlugHrmAttendance $attendance = null;
#[ORM\Column(length: 10)]
private ?string $type = null; // ورود یا خروج
#[ORM\Column(length: 5)]
private ?string $time = null; // زمان HH:MM
#[ORM\Column(type: Types::INTEGER)]
private ?int $timestamp = null; // unix timestamp
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
private ?\DateTimeInterface $createdAt = null;
public function __construct()
{
$this->createdAt = new \DateTime();
}
public function getId(): ?int
{
return $this->id;
}
public function getAttendance(): ?PlugHrmAttendance
{
return $this->attendance;
}
public function setAttendance(?PlugHrmAttendance $attendance): static
{
$this->attendance = $attendance;
return $this;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(string $type): static
{
$this->type = $type;
return $this;
}
public function getTime(): ?string
{
return $this->time;
}
public function setTime(string $time): static
{
$this->time = $time;
return $this;
}
public function getTimestamp(): ?int
{
return $this->timestamp;
}
public function setTimestamp(int $timestamp): static
{
$this->timestamp = $timestamp;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
}

View file

@ -82,6 +82,12 @@ class PlugWarrantySerial
#[ORM\ManyToOne]
private ?Person $buyer = null;
#[ORM\Column(type: 'string', length: 50, nullable: true)]
private ?string $allocatedToDocumentType = null;
#[ORM\ManyToOne]
private ?User $allocatedBy = null;
#[ORM\Column(type: 'string', length: 32, nullable: true)]
private ?string $activationTicketCode = null;
@ -152,6 +158,12 @@ class PlugWarrantySerial
public function getBuyer(): ?Person { return $this->buyer; }
public function setBuyer(?Person $buyer): self { $this->buyer = $buyer; return $this; }
public function getAllocatedToDocumentType(): ?string { return $this->allocatedToDocumentType; }
public function setAllocatedToDocumentType(?string $type): self { $this->allocatedToDocumentType = $type; return $this; }
public function getAllocatedBy(): ?User { return $this->allocatedBy; }
public function setAllocatedBy(?User $user): self { $this->allocatedBy = $user; return $this; }
public function getActivationTicketCode(): ?string { return $this->activationTicketCode; }
public function setActivationTicketCode(?string $code): self { $this->activationTicketCode = $code; return $this; }
public function getActivationTicketSecret(): ?string { return $this->activationTicketSecret; }

View file

@ -96,6 +96,17 @@ class StoreroomTicket
#[ORM\JoinColumn(nullable: true)]
private ?User $approvedBy = null;
// Completion fields
#[ORM\Column(nullable: true)]
private ?bool $completed = null;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $completedAt = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true)]
private ?User $completedBy = null;
public function __construct()
{
$this->storeroomItems = new ArrayCollection();
@ -409,4 +420,38 @@ class StoreroomTicket
$this->approvedBy = $approvedBy;
return $this;
}
// Completion methods
public function isCompleted(): ?bool
{
return $this->completed;
}
public function setCompleted(?bool $completed): static
{
$this->completed = $completed;
return $this;
}
public function getCompletedAt(): ?\DateTimeImmutable
{
return $this->completedAt;
}
public function setCompletedAt(?\DateTimeImmutable $completedAt): static
{
$this->completedAt = $completedAt;
return $this;
}
public function getCompletedBy(): ?User
{
return $this->completedBy;
}
public function setCompletedBy(?User $completedBy): static
{
$this->completedBy = $completedBy;
return $this;
}
}

View file

@ -1,43 +1,43 @@
<?php
namespace App\Repository;
// namespace App\Repository;
use App\Entity\HesabdariDoc;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
// use App\Entity\HesabdariDoc;
// use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
// use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<HesabdariDoc>
*
* @method HesabdariDoc|null find($id, $lockMode = null, $lockVersion = null)
* @method HesabdariDoc|null findOneBy(array $criteria, array $orderBy = null)
* @method HesabdariDoc[] findAll()
* @method HesabdariDoc[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class HesabdariDocRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, HesabdariDoc::class);
}
// /**
// * @extends ServiceEntityRepository<HesabdariDoc>
// *
// * @method HesabdariDoc|null find($id, $lockMode = null, $lockVersion = null)
// * @method HesabdariDoc|null findOneBy(array $criteria, array $orderBy = null)
// * @method HesabdariDoc[] findAll()
// * @method HesabdariDoc[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
// */
// class HesabdariDocRepository extends ServiceEntityRepository
// {
// public function __construct(ManagerRegistry $registry)
// {
// parent::__construct($registry, HesabdariDoc::class);
// }
public function save(HesabdariDoc $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
// public function save(HesabdariDoc $entity, bool $flush = false): void
// {
// $this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// if ($flush) {
// $this->getEntityManager()->flush();
// }
// }
public function remove(HesabdariDoc $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
// public function remove(HesabdariDoc $entity, bool $flush = false): void
// {
// $this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// if ($flush) {
// $this->getEntityManager()->flush();
// }
// }
// /**
// * @return HesabdariDoc[] Returns an array of HesabdariDoc objects
@ -63,4 +63,158 @@ class HesabdariDocRepository extends ServiceEntityRepository
// ->getOneOrNullResult()
// ;
// }
}
// }
namespace App\Repository;
use App\Entity\HesabdariDoc;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class HesabdariDocRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, HesabdariDoc::class);
}
public function save(HesabdariDoc $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(HesabdariDoc $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function find(mixed $id, \Doctrine\DBAL\LockMode|int|null $lockMode = null, ?int $lockVersion = null): ?object
{
return $this->createQueryBuilder('h')
->andWhere('h.id = :id')
->andWhere('h.isApproved = 1')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult();
}
public function findOneBy(array $criteria, ?array $orderBy = null): ?object
{
$qb = $this->createQueryBuilder('h');
foreach ($criteria as $field => $value) {
if ($field === 'bid' && is_object($value)) {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value->getId());
} else {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value);
}
}
$qb->andWhere('h.isApproved = 1');
if ($orderBy) {
foreach ($orderBy as $field => $direction) {
$qb->addOrderBy("h.$field", $direction);
}
}
return $qb->getQuery()->getOneOrNullResult();
}
//include preview
public function findOneByIncludePreview(array $criteria, ?array $orderBy = null): ?object
{
$qb = $this->createQueryBuilder('h');
foreach ($criteria as $field => $value) {
if ($field === 'bid' && is_object($value)) {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value->getId());
} else {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value);
}
}
if ($orderBy) {
foreach ($orderBy as $field => $direction) {
$qb->addOrderBy("h.$field", $direction);
}
}
return $qb->getQuery()->getOneOrNullResult();
}
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
$qb = $this->createQueryBuilder('h');
foreach ($criteria as $field => $value) {
if ($field === 'bid' && is_object($value)) {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value->getId());
} else {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value);
}
}
$qb->andWhere('h.isApproved = 1');
if ($orderBy) {
foreach ($orderBy as $field => $direction) {
$qb->addOrderBy("h.$field", $direction);
}
}
if ($limit) {
$qb->setMaxResults($limit);
}
if ($offset) {
$qb->setFirstResult($offset);
}
return $qb->getQuery()->getResult();
}
public function findAll(): array
{
return $this->createQueryBuilder('h')
->andWhere('h.isApproved = 1')
->getQuery()
->getResult();
}
//include preview
public function findByIncludePreview(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
$qb = $this->createQueryBuilder('h');
foreach ($criteria as $field => $value) {
if ($field === 'bid' && is_object($value)) {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value->getId());
} else {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value);
}
}
if ($orderBy) {
foreach ($orderBy as $field => $direction) {
$qb->addOrderBy("h.$field", $direction);
}
}
if ($limit) {
$qb->setMaxResults($limit);
}
if ($offset) {
$qb->setFirstResult($offset);
}
return $qb->getQuery()->getResult();
}
}

View file

@ -33,7 +33,7 @@ class HesabdariRowRepository extends ServiceEntityRepository
/**
* پیدا کردن ردیف‌ها با جوین روی سند و فیلتر پول، با حذف تکرارها
*/
public function findByJoinMoney(array $params, Money $money): array
public function findByJoinMoney(array $params, Money $money, ?array $dateFilter = null): array
{
$query = $this->createQueryBuilder('t')
->select('DISTINCT t') // حذف تکرارها با DISTINCT
@ -56,6 +56,14 @@ class HesabdariRowRepository extends ServiceEntityRepository
}
}
// اضافه کردن فیلتر تاریخ اگر موجود باشد
if ($dateFilter && isset($dateFilter['startDate']) && isset($dateFilter['endDate'])) {
$query->andWhere('d.date >= :startDate')
->andWhere('d.date <= :endDate')
->setParameter('startDate', $dateFilter['startDate'])
->setParameter('endDate', $dateFilter['endDate']);
}
return $query->getQuery()->getResult();
}

View file

@ -0,0 +1,144 @@
<?php
namespace App\Repository;
use App\Entity\OAuthAccessToken;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<OAuthAccessToken>
*
* @method OAuthAccessToken|null find($id, $lockMode = null, $lockVersion = null)
* @method OAuthAccessToken|null findOneBy(array $criteria, array $orderBy = null)
* @method OAuthAccessToken[] findAll()
* @method OAuthAccessToken[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class OAuthAccessTokenRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, OAuthAccessToken::class);
}
public function save(OAuthAccessToken $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(OAuthAccessToken $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function findByToken(string $token): ?OAuthAccessToken
{
return $this->findOneBy(['token' => $token]);
}
public function findValidByToken(string $token): ?OAuthAccessToken
{
$qb = $this->createQueryBuilder('at')
->andWhere('at.token = :token')
->andWhere('at.isRevoked = :isRevoked')
->andWhere('at.expiresAt > :now')
->setParameter('token', $token)
->setParameter('isRevoked', false)
->setParameter('now', new \DateTime());
return $qb->getQuery()->getOneOrNullResult();
}
public function findByRefreshToken(string $refreshToken): ?OAuthAccessToken
{
return $this->findOneBy(['refreshToken' => $refreshToken]);
}
public function findValidByRefreshToken(string $refreshToken): ?OAuthAccessToken
{
$qb = $this->createQueryBuilder('at')
->andWhere('at.refreshToken = :refreshToken')
->andWhere('at.isRevoked = :isRevoked')
->andWhere('at.expiresAt > :now')
->setParameter('refreshToken', $refreshToken)
->setParameter('isRevoked', false)
->setParameter('now', new \DateTime());
return $qb->getQuery()->getOneOrNullResult();
}
public function findByUser(int $userId): array
{
return $this->createQueryBuilder('at')
->andWhere('at.user = :userId')
->setParameter('userId', $userId)
->orderBy('at.createdAt', 'DESC')
->getQuery()
->getResult();
}
public function findByApplication(int $applicationId): array
{
return $this->createQueryBuilder('at')
->andWhere('at.application = :applicationId')
->setParameter('applicationId', $applicationId)
->orderBy('at.createdAt', 'DESC')
->getQuery()
->getResult();
}
public function findExpiredTokens(): array
{
return $this->createQueryBuilder('at')
->andWhere('at.expiresAt < :now')
->setParameter('now', new \DateTime())
->getQuery()
->getResult();
}
public function cleanupExpiredTokens(): int
{
$qb = $this->createQueryBuilder('at')
->delete()
->andWhere('at.expiresAt < :now')
->setParameter('now', new \DateTime());
return $qb->getQuery()->execute();
}
public function revokeUserTokens(int $userId): int
{
$qb = $this->createQueryBuilder('at')
->update()
->set('at.isRevoked', ':isRevoked')
->andWhere('at.user = :userId')
->andWhere('at.isRevoked = :currentRevoked')
->setParameter('isRevoked', true)
->setParameter('userId', $userId)
->setParameter('currentRevoked', false);
return $qb->getQuery()->execute();
}
public function revokeApplicationTokens(int $applicationId): int
{
$qb = $this->createQueryBuilder('at')
->update()
->set('at.isRevoked', ':isRevoked')
->andWhere('at.application = :applicationId')
->andWhere('at.isRevoked = :currentRevoked')
->setParameter('isRevoked', true)
->setParameter('applicationId', $applicationId)
->setParameter('currentRevoked', false);
return $qb->getQuery()->execute();
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace App\Repository;
use App\Entity\OAuthApplication;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<OAuthApplication>
*
* @method OAuthApplication|null find($id, $lockMode = null, $lockVersion = null)
* @method OAuthApplication|null findOneBy(array $criteria, array $orderBy = null)
* @method OAuthApplication[] findAll()
* @method OAuthApplication[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class OAuthApplicationRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, OAuthApplication::class);
}
public function save(OAuthApplication $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(OAuthApplication $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function findByClientId(string $clientId): ?OAuthApplication
{
return $this->findOneBy(['clientId' => $clientId, 'isActive' => true]);
}
public function findByOwner(int $ownerId): array
{
return $this->createQueryBuilder('o')
->andWhere('o.owner = :ownerId')
->setParameter('ownerId', $ownerId)
->orderBy('o.createdAt', 'DESC')
->getQuery()
->getResult();
}
public function findActiveApplications(): array
{
return $this->createQueryBuilder('o')
->andWhere('o.isActive = :active')
->setParameter('active', true)
->orderBy('o.name', 'ASC')
->getQuery()
->getResult();
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace App\Repository;
use App\Entity\OAuthAuthorizationCode;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<OAuthAuthorizationCode>
*
* @method OAuthAuthorizationCode|null find($id, $lockMode = null, $lockVersion = null)
* @method OAuthAuthorizationCode|null findOneBy(array $criteria, array $orderBy = null)
* @method OAuthAuthorizationCode[] findAll()
* @method OAuthAuthorizationCode[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class OAuthAuthorizationCodeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, OAuthAuthorizationCode::class);
}
public function save(OAuthAuthorizationCode $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(OAuthAuthorizationCode $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function findByCode(string $code): ?OAuthAuthorizationCode
{
return $this->findOneBy(['code' => $code]);
}
public function findValidByCode(string $code): ?OAuthAuthorizationCode
{
$qb = $this->createQueryBuilder('ac')
->andWhere('ac.code = :code')
->andWhere('ac.isUsed = :isUsed')
->andWhere('ac.expiresAt > :now')
->setParameter('code', $code)
->setParameter('isUsed', false)
->setParameter('now', new \DateTime());
return $qb->getQuery()->getOneOrNullResult();
}
public function findExpiredCodes(): array
{
return $this->createQueryBuilder('ac')
->andWhere('ac.expiresAt < :now')
->setParameter('now', new \DateTime())
->getQuery()
->getResult();
}
public function cleanupExpiredCodes(): int
{
$qb = $this->createQueryBuilder('ac')
->delete()
->andWhere('ac.expiresAt < :now')
->setParameter('now', new \DateTime());
return $qb->getQuery()->execute();
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace App\Repository;
use App\Entity\OAuthScope;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<OAuthScope>
*
* @method OAuthScope|null find($id, $lockMode = null, $lockVersion = null)
* @method OAuthScope|null findOneBy(array $criteria, array $orderBy = null)
* @method OAuthScope[] findAll()
* @method OAuthScope[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class OAuthScopeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, OAuthScope::class);
}
public function save(OAuthScope $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(OAuthScope $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function findByName(string $name): ?OAuthScope
{
return $this->findOneBy(['name' => $name]);
}
public function findDefaultScopes(): array
{
return $this->createQueryBuilder('s')
->andWhere('s.isDefault = :isDefault')
->setParameter('isDefault', true)
->orderBy('s.name', 'ASC')
->getQuery()
->getResult();
}
public function findSystemScopes(): array
{
return $this->createQueryBuilder('s')
->andWhere('s.isSystem = :isSystem')
->setParameter('isSystem', true)
->orderBy('s.name', 'ASC')
->getQuery()
->getResult();
}
public function findByNames(array $names): array
{
return $this->createQueryBuilder('s')
->andWhere('s.name IN (:names)')
->setParameter('names', $names)
->getQuery()
->getResult();
}
}

View file

@ -47,7 +47,7 @@ class PersonRepository extends ServiceEntityRepository
{
return $this->createQueryBuilder('p')
->where('p.bid = :val')
->andWhere("p.nikename LIKE :search OR p.mobile LIKE :search")
->andWhere("p.nikename LIKE :search OR p.mobile LIKE :search OR p.paymentId LIKE :search")
->setParameter('val', $bid)
->setParameter('search', '%' . $search . '%')
->setMaxResults($maxResults)

View file

@ -0,0 +1,54 @@
<?php
namespace App\Repository;
use App\Entity\PlugHrmAttendanceItem;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<PlugHrmAttendanceItem>
*
* @method PlugHrmAttendanceItem|null find($id, $lockMode = null, $lockVersion = null)
* @method PlugHrmAttendanceItem|null findOneBy(array $criteria, array $orderBy = null)
* @method PlugHrmAttendanceItem[] findAll()
* @method PlugHrmAttendanceItem[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PlugHrmAttendanceItemRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, PlugHrmAttendanceItem::class);
}
public function save(PlugHrmAttendanceItem $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(PlugHrmAttendanceItem $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* @return PlugHrmAttendanceItem[] Returns an array of PlugHrmAttendanceItem objects
*/
public function findByAttendanceOrderedByTime($attendance): array
{
return $this->createQueryBuilder('i')
->where('i.attendance = :attendance')
->setParameter('attendance', $attendance)
->orderBy('i.time', 'ASC')
->getQuery()
->getResult();
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace App\Repository;
use App\Entity\PlugHrmAttendance;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<PlugHrmAttendance>
*
* @method PlugHrmAttendance|null find($id, $lockMode = null, $lockVersion = null)
* @method PlugHrmAttendance|null findOneBy(array $criteria, array $orderBy = null)
* @method PlugHrmAttendance[] findAll()
* @method PlugHrmAttendance[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PlugHrmAttendanceRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, PlugHrmAttendance::class);
}
public function save(PlugHrmAttendance $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(PlugHrmAttendance $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* @return PlugHrmAttendance[] Returns an array of PlugHrmAttendance objects
*/
public function findByBusinessAndDateRange($business, $fromDate, $toDate, $personId = null): array
{
$qb = $this->createQueryBuilder('a')
->leftJoin('a.person', 'p')
->addSelect('p')
->where('a.business = :business')
->andWhere('a.date >= :fromDate')
->andWhere('a.date <= :toDate')
->setParameter('business', $business)
->setParameter('fromDate', $fromDate)
->setParameter('toDate', $toDate)
->orderBy('a.date', 'DESC')
->addOrderBy('p.nikename', 'ASC');
if ($personId) {
$qb->andWhere('a.person = :personId')
->setParameter('personId', $personId);
}
return $qb->getQuery()->getResult();
}
/**
* @return PlugHrmAttendance[] Returns an array of PlugHrmAttendance objects
*/
public function findByBusinessAndPerson($business, $person, $limit = 30): array
{
return $this->createQueryBuilder('a')
->where('a.business = :business')
->andWhere('a.person = :person')
->setParameter('business', $business)
->setParameter('person', $person)
->orderBy('a.date', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
/**
* @return PlugHrmAttendance|null Returns a single PlugHrmAttendance object
*/
public function findByBusinessAndPersonAndDate($business, $person, $date): ?PlugHrmAttendance
{
return $this->createQueryBuilder('a')
->where('a.business = :business')
->andWhere('a.person = :person')
->andWhere('a.date = :date')
->setParameter('business', $business)
->setParameter('person', $person)
->setParameter('date', $date)
->getQuery()
->getOneOrNullResult();
}
}

View file

@ -29,7 +29,7 @@ class WalletTransactionRepository extends ServiceEntityRepository
{
return $this->createQueryBuilder('w')
->andWhere('w.bid = :val')
->andWhere("w.type != 'pay'")
->andWhere("w.type = 'sell'")
->setParameter('val', $business)
->orderBy('w.id', 'DESC')
->getQuery()
@ -37,6 +37,31 @@ class WalletTransactionRepository extends ServiceEntityRepository
;
}
public function calculateWalletBalance(Business $business): float
{
$qb = $this->createQueryBuilder('w');
// محاسبه مجموع تراکنش‌های sell (درآمد)
$incomeQuery = $qb->select('SUM(CAST(w.amount AS DECIMAL(10,2)))')
->where('w.bid = :business')
->andWhere("w.type = 'sell'")
->setParameter('business', $business)
->getQuery();
$income = $incomeQuery->getSingleScalarResult() ?? 0;
// محاسبه مجموع تراکنش‌های pay (هزینه)
$expenseQuery = $qb->select('SUM(CAST(w.amount AS DECIMAL(10,2)))')
->where('w.bid = :business')
->andWhere("w.type = 'pay'")
->setParameter('business', $business)
->getQuery();
$expense = $expenseQuery->getSingleScalarResult() ?? 0;
return (float) $income - (float) $expense;
}
// public function findOneBySomeField($value): ?WalletTransaction
// {
// return $this->createQueryBuilder('w')

View file

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Security;
use App\Service\OAuthService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class OAuthAuthenticator extends AbstractAuthenticator
{
private OAuthService $oauthService;
public function __construct(OAuthService $oauthService)
{
$this->oauthService = $oauthService;
}
/**
* Called on every request to decide if this authenticator should be
* used for the request. Returning `false` will cause this authenticator
* to be skipped.
*/
public function supports(Request $request): ?bool
{
$authorization = $request->headers->get('Authorization');
return $authorization && str_starts_with($authorization, 'Bearer ');
}
public function authenticate(Request $request): Passport
{
$authorization = $request->headers->get('Authorization');
$token = substr($authorization, 7);
if (empty($token)) {
throw new CustomUserMessageAuthenticationException('No Bearer token provided');
}
return new SelfValidatingPassport(
new UserBadge($token, function($token) {
$accessToken = $this->oauthService->validateAccessToken($token);
if (!$accessToken) {
throw new UserNotFoundException('Invalid access token');
}
return $accessToken->getUser();
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// on success, let the request continue
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$data = [
'error' => 'invalid_token',
'error_description' => $exception->getMessage()
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
}

View file

@ -137,7 +137,7 @@ class Access
'user'=>$this->user
]);
if($warehousePermission && $warehousePermission->isWarehouseManager()){
if($warehousePermission && $warehousePermission->isStorehelper()){
$warehouseRoles = ['commodity', 'store', 'plugWarrantyManager'];
if(in_array($roll, $warehouseRoles)){
return $accessArray;

File diff suppressed because it is too large Load diff

View file

@ -333,7 +333,7 @@ class Explore
'id' => $person->getId(),
'code' => $person->getCode(),
'nikename' => $person->getNikename(),
'name' => $person->getName(),
'name' => $person->getName() ?: $person->getNikename(),
'tel' => $person->getTel(),
'mobile' => $person->getmobile(),
'mobile2' => $person->getMobile2(),
@ -354,6 +354,7 @@ class Explore
'address' => $person->getAddress(),
'prelabel' => null,
'tags' => $person->getTags(),
'paymentId' => $person->getPaymentId(),
'requireTwoStep' => $person->getBid() ? $person->getBid()->isRequireTwoStepApproval() : false,
];
if ($person->getPrelabel()) {
@ -579,11 +580,22 @@ class Explore
'walletEnabled' => $item->isWalletEnable(),
'walletMatchBank' => $item->getWalletMatchBank() ? $item->getWalletMatchBank()->getId() : null,
'requireTwoStepApproval' => $item->isRequireTwoStepApproval(),
'invoiceApprover' => $item->getInvoiceApprover(),
'warehouseApprover' => $item->getWarehouseApprover(),
'financialApprover' => $item->getFinancialApprover(),
'approvers' => [
'sellInvoice' => $item->getApproverSellInvoice(),
'buyInvoice' => $item->getApproverBuyInvoice(),
'returnBuy' => $item->getApproverReturnBuy(),
'returnSell' => $item->getApproverReturnSell(),
'warehouseTransfer' => $item->getApproverWarehouseTransfer(),
'receiveFromPersons' => $item->getApproverReceiveFromPersons(),
'payToPersons' => $item->getApproverPayToPersons(),
'accountingDocs' => $item->getApproverAccountingDocs(),
'bankTransfers' => $item->getApproverBankTransfers(),
],
'updateSellPrice' => $item->isCommodityUpdateSellPriceAuto(),
'updateBuyPrice' => $item->isCommodityUpdateBuyPriceAuto(),
'requireWarrantyOnDelivery' => $item->getRequireWarrantyOnDelivery(),
'activationGraceDays' => $item->getActivationGraceDays(),
'matchWarrantyToSerial' => $item->getMatchWarrantyToSerial(),
];
if (!$item->getProfitCalctype()) {
$res['profitCalcType'] = 'lis';
@ -645,6 +657,11 @@ class Explore
'topCostCenters' => $item->isTopCostCenters(),
'incomes' => $item->isIncomes(),
'topIncomeCenters' => $item->isTopIncomesChart(),
'cheques' => $item->isCheques(),
'chequesDueToday' => $item->isChequesDueToday(),
'chequesStatusChart' => $item->isChequesStatusChart(),
'chequesMonthlyChart' => $item->isChequesMonthlyChart(),
'chequesDueSoon' => $item->isChequesDueSoon(),
];
if ($result['topCommodities'] === null)
$result['topCommodities'] = true;
@ -676,6 +693,16 @@ class Explore
$result['incomes'] = true;
if ($result['topIncomeCenters'] === null)
$result['topIncomeCenters'] = true;
if ($result['cheques'] === null)
$result['cheques'] = true;
if ($result['chequesDueToday'] === null)
$result['chequesDueToday'] = true;
if ($result['chequesStatusChart'] === null)
$result['chequesStatusChart'] = true;
if ($result['chequesMonthlyChart'] === null)
$result['chequesMonthlyChart'] = true;
if ($result['chequesDueSoon'] === null)
$result['chequesDueSoon'] = true;
return $result;
}

View file

@ -15,7 +15,7 @@ class FileStorage
{
$safeOriginal = preg_replace('/[^A-Za-z0-9_.-]/', '_', $file->getClientOriginalName());
$relativeDir = 'storage/' . trim($businessId) . '/' . trim($context);
$absDir = rtrim($this->kernel->getProjectDir(), '/').'/var/' . $relativeDir;
$absDir = rtrim($this->kernel->getProjectDir(), '/').'/../hesabixArchive/' . $relativeDir;
if (!is_dir($absDir)) {
@mkdir($absDir, 0775, true);
}
@ -35,7 +35,7 @@ class FileStorage
public function absolutePath(string $relativePath): string
{
$relativePath = ltrim($relativePath, '/');
return rtrim($this->kernel->getProjectDir(), '/').'/var/' . $relativePath;
return rtrim($this->kernel->getProjectDir(), '/').'/../hesabixArchive/' . $relativePath;
}
}

View file

@ -36,4 +36,24 @@ class Log
$this->em->persist($log);
$this->em->flush();
}
/**
* ثبت لاگ برای عملیات پیش‌فاکتور
*/
public function insertPreInvoiceLog(string $part, string $des, User | null $user = null, Business | string | null $bid = null): void
{
if(is_string($bid))
$bid = $this->em->getRepository(Business::class)->find($bid);
$log = new \App\Entity\Log();
$log->setDateSubmit(time());
$log->setPart($part);
$log->setDes($des);
$log->setUser($user);
$log->setBid($bid);
$log->setDoc(null); // برای پیش‌فاکتور، doc را null قرار می‌دهیم
$log->setRepserviceOrder(null);
$log->setIpaddress($this->remoteAddress->getIpAddress());
$this->em->persist($log);
$this->em->flush();
}
}

View file

@ -0,0 +1,303 @@
<?php
namespace App\Service;
use App\Entity\OAuthApplication;
use App\Entity\OAuthAuthorizationCode;
use App\Entity\OAuthAccessToken;
use App\Entity\OAuthScope;
use App\Entity\User;
use App\Repository\OAuthApplicationRepository;
use App\Repository\OAuthAuthorizationCodeRepository;
use App\Repository\OAuthAccessTokenRepository;
use App\Repository\OAuthScopeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class OAuthService
{
private EntityManagerInterface $entityManager;
private OAuthApplicationRepository $applicationRepository;
private OAuthAuthorizationCodeRepository $authorizationCodeRepository;
private OAuthAccessTokenRepository $accessTokenRepository;
private OAuthScopeRepository $scopeRepository;
public function __construct(
EntityManagerInterface $entityManager,
OAuthApplicationRepository $applicationRepository,
OAuthAuthorizationCodeRepository $authorizationCodeRepository,
OAuthAccessTokenRepository $accessTokenRepository,
OAuthScopeRepository $scopeRepository
) {
$this->entityManager = $entityManager;
$this->applicationRepository = $applicationRepository;
$this->authorizationCodeRepository = $authorizationCodeRepository;
$this->accessTokenRepository = $accessTokenRepository;
$this->scopeRepository = $scopeRepository;
}
/**
* تولید client_id و client_secret برای برنامه جدید
*/
public function generateClientCredentials(): array
{
$clientId = $this->generateRandomString(32);
$clientSecret = $this->generateRandomString(64);
return [
'client_id' => $clientId,
'client_secret' => $clientSecret
];
}
/**
* اعتبارسنجی درخواست authorization
*/
public function validateAuthorizationRequest(Request $request): array
{
$clientId = $request->query->get('client_id');
$redirectUri = $request->query->get('redirect_uri');
$responseType = $request->query->get('response_type');
$scope = $request->query->get('scope');
$state = $request->query->get('state');
// بررسی وجود پارامترهای اجباری
if (!$clientId || !$redirectUri || !$responseType) {
throw new AuthenticationException('Missing required parameters');
}
// بررسی نوع response
if ($responseType !== 'code') {
throw new AuthenticationException('Unsupported response_type');
}
// بررسی وجود برنامه
$application = $this->applicationRepository->findByClientId($clientId);
if (!$application) {
throw new AuthenticationException('Invalid client_id');
}
// بررسی فعال بودن برنامه
if (!$application->isActive()) {
throw new AuthenticationException('Application is not active');
}
// بررسی redirect_uri
if ($application->getRedirectUri() !== $redirectUri) {
throw new AuthenticationException('Invalid redirect_uri');
}
// بررسی محدوده‌های دسترسی
$requestedScopes = $scope ? explode(' ', $scope) : [];
$allowedScopes = $application->getAllowedScopes();
$validScopes = array_intersect($requestedScopes, $allowedScopes);
if (empty($validScopes) && !empty($requestedScopes)) {
throw new AuthenticationException('Invalid scope');
}
return [
'application' => $application,
'scopes' => $validScopes,
'state' => $state
];
}
/**
* ایجاد کد مجوز
*/
public function createAuthorizationCode(User $user, OAuthApplication $application, array $scopes, ?string $state = null): OAuthAuthorizationCode
{
$code = new OAuthAuthorizationCode();
$code->setCode($this->generateRandomString(64));
$code->setUser($user);
$code->setApplication($application);
$code->setRedirectUri($application->getRedirectUri());
$code->setScopes($scopes);
$code->setState($state);
$code->setExpiresAt((new \DateTime())->modify('+10 minutes'));
$this->entityManager->persist($code);
$this->entityManager->flush();
return $code;
}
/**
* اعتبارسنجی کد مجوز
*/
public function validateAuthorizationCode(string $code, string $clientId, string $redirectUri): ?OAuthAuthorizationCode
{
$authorizationCode = $this->authorizationCodeRepository->findValidByCode($code);
if (!$authorizationCode) {
return null;
}
$application = $authorizationCode->getApplication();
// بررسی تطابق client_id
if ($application->getClientId() !== $clientId) {
return null;
}
// بررسی تطابق redirect_uri
if ($authorizationCode->getRedirectUri() !== $redirectUri) {
return null;
}
return $authorizationCode;
}
/**
* استفاده از کد مجوز
*/
public function useAuthorizationCode(OAuthAuthorizationCode $authorizationCode): void
{
$authorizationCode->setIsUsed(true);
$this->entityManager->flush();
}
/**
* ایجاد توکن دسترسی
*/
public function createAccessToken(User $user, OAuthApplication $application, array $scopes): OAuthAccessToken
{
$token = new OAuthAccessToken();
$token->setToken($this->generateRandomString(64));
$token->setRefreshToken($this->generateRandomString(64));
$token->setUser($user);
$token->setApplication($application);
$token->setScopes($scopes);
$token->setExpiresAt((new \DateTime())->modify('+1 hour'));
$this->entityManager->persist($token);
$this->entityManager->flush();
return $token;
}
/**
* تمدید توکن دسترسی
*/
public function refreshAccessToken(string $refreshToken): ?OAuthAccessToken
{
$oldToken = $this->accessTokenRepository->findValidByRefreshToken($refreshToken);
if (!$oldToken) {
return null;
}
// ایجاد توکن جدید
$newToken = new OAuthAccessToken();
$newToken->setToken($this->generateRandomString(64));
$newToken->setRefreshToken($this->generateRandomString(64));
$newToken->setUser($oldToken->getUser());
$newToken->setApplication($oldToken->getApplication());
$newToken->setScopes($oldToken->getScopes());
$newToken->setExpiresAt((new \DateTime())->modify('+1 hour'));
// لغو توکن قدیمی
$oldToken->setIsRevoked(true);
$this->entityManager->persist($newToken);
$this->entityManager->flush();
return $newToken;
}
/**
* اعتبارسنجی توکن دسترسی
*/
public function validateAccessToken(string $token): ?OAuthAccessToken
{
$accessToken = $this->accessTokenRepository->findValidByToken($token);
if ($accessToken) {
$accessToken->updateLastUsed();
$this->entityManager->flush();
}
return $accessToken;
}
/**
* لغو توکن دسترسی
*/
public function revokeAccessToken(string $token): bool
{
$accessToken = $this->accessTokenRepository->findByToken($token);
if (!$accessToken) {
return false;
}
$accessToken->setIsRevoked(true);
$this->entityManager->flush();
return true;
}
/**
* ایجاد محدوده‌های پیش‌فرض سیستم
*/
public function createDefaultScopes(): void
{
$defaultScopes = [
'read_profile' => 'دسترسی به اطلاعات پروفایل کاربر',
'write_profile' => 'ویرایش اطلاعات پروفایل کاربر',
'read_business' => 'دسترسی به اطلاعات کسب‌وکار',
'write_business' => 'ویرایش اطلاعات کسب‌وکار',
'read_accounting' => 'دسترسی به اطلاعات حسابداری',
'write_accounting' => 'ویرایش اطلاعات حسابداری',
'read_reports' => 'دسترسی به گزارش‌ها',
'write_reports' => 'ایجاد و ویرایش گزارش‌ها',
'admin' => 'دسترسی مدیریتی کامل'
];
foreach ($defaultScopes as $name => $description) {
$existingScope = $this->scopeRepository->findByName($name);
if (!$existingScope) {
$scope = new OAuthScope();
$scope->setName($name);
$scope->setDescription($description);
$scope->setIsSystem(true);
$scope->setIsDefault(in_array($name, ['read_profile', 'read_business']));
$this->entityManager->persist($scope);
}
}
$this->entityManager->flush();
}
/**
* تولید رشته تصادفی
*/
private function generateRandomString(int $length): string
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$string = '';
for ($i = 0; $i < $length; $i++) {
$string .= $characters[random_int(0, strlen($characters) - 1)];
}
return $string;
}
/**
* پاکسازی کدها و توکن‌های منقضی شده
*/
public function cleanupExpiredItems(): array
{
$expiredCodes = $this->authorizationCodeRepository->cleanupExpiredCodes();
$expiredTokens = $this->accessTokenRepository->cleanupExpiredTokens();
return [
'expired_codes' => $expiredCodes,
'expired_tokens' => $expiredTokens
];
}
}

View file

@ -0,0 +1,285 @@
<?php
namespace App\Service;
use App\Entity\PreInvoiceDoc;
use App\Entity\HesabdariDoc;
use App\Entity\HesabdariRow;
use App\Entity\HesabdariTable;
use App\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class PreInvoiceConversionService
{
private EntityManagerInterface $entityManager;
private Provider $provider;
private Log $log;
public function __construct(
EntityManagerInterface $entityManager,
Provider $provider,
Log $log
) {
$this->entityManager = $entityManager;
$this->provider = $provider;
$this->log = $log;
}
/**
* تبدیل پیش‌فاکتور به فاکتور فروش
*/
public function convertToInvoice(PreInvoiceDoc $preInvoice, UserInterface $user, array $acc): array
{
try {
// بررسی وضعیت پیش‌فاکتور
if ($preInvoice->getStatus() === 'converted') {
throw new \Exception('این پیش‌فاکتور قبلاً به فاکتور فروش تبدیل شده است');
}
// ایجاد فاکتور فروش جدید
$sellDoc = new HesabdariDoc();
$sellDoc->setBid($acc['bid']);
$sellDoc->setYear($acc['year']);
$sellDoc->setDateSubmit(time());
$sellDoc->setType('sell');
$sellDoc->setSubmitter($user);
$sellDoc->setMoney($acc['money']);
$sellDoc->setCode($this->provider->getAccountingCode($acc['bid'], 'accounting'));
$sellDoc->setDate($preInvoice->getDate());
$sellDoc->setDes($preInvoice->getDes() ?: 'فاکتور فروش تبدیل شده از پیش‌فاکتور شماره ' . $preInvoice->getCode());
// مبلغ کل موقت - بعداً به‌روزرسانی می‌شود
$sellDoc->setAmount($preInvoice->getAmount());
$sellDoc->setTaxPercent($preInvoice->getTaxPercent() ?: 0);
$sellDoc->setStatus('approved');
// تنظیم تخفیف کل
if ($preInvoice->getTotalDiscount() || $preInvoice->getTotalDiscountPercent()) {
$sellDoc->setDiscountType($preInvoice->isShowTotalPercentDiscount() ? 'percent' : 'amount');
$sellDoc->setDiscountPercent(floatval($preInvoice->getTotalDiscountPercent() ?: 0));
}
// تنظیم برچسب فاکتور اگر وجود داشته باشد
if ($preInvoice->getInvoiceLabel()) {
$sellDoc->setInvoiceLabel($preInvoice->getInvoiceLabel());
}
// تنظیم فروشنده اگر وجود داشته باشد
if ($preInvoice->getSalesman()) {
$sellDoc->setSalesman($preInvoice->getSalesman());
}
$this->entityManager->persist($sellDoc);
// تبدیل آیتم‌های پیش‌فاکتور به ردیف‌های فاکتور فروش
$sumTotal = 0;
$sumTax = 0;
$totalItemDiscount = 0;
// تعریف متغیرهای تخفیف و هزینه حمل
$totalDiscountAmount = floatval($preInvoice->getTotalDiscount() ?: 0);
$shippingCost = floatval($preInvoice->getShippingCost() ?: 0);
foreach ($preInvoice->getPreInvoiceItems() as $preItem) {
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDes($preItem->getDes() ?: 'فروش کالا');
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($sellDoc);
$itemTotal = $preItem->getCommodityCount() * $preItem->getBs();
$itemDiscount = $preItem->getDiscountAmount() ?: 0;
// محاسبه تخفیف درصدی اگر تخفیف مقداری صفر باشد
if ($itemDiscount == 0 && $preItem->getDiscountPercent() && $preItem->getDiscountPercent() > 0) {
$itemDiscount = ($itemTotal * floatval($preItem->getDiscountPercent())) / 100;
}
$itemTotalAfterDiscount = $itemTotal - $itemDiscount;
// ذخیره مبلغ کل کالا بعد از تخفیف در فیلد bs - مطابق با انتظار Controller
$hesabdariRow->setBs($itemTotalAfterDiscount);
$hesabdariRow->setBd(0);
$hesabdariRow->setCommdityCount($preItem->getCommodityCount());
$hesabdariRow->setCommodity($preItem->getCommodity());
$hesabdariRow->setDiscount($itemDiscount);
$hesabdariRow->setDiscountPercent($preItem->getDiscountPercent() ?: 0);
$hesabdariRow->setDiscountType($preItem->isShowPercentDiscount() ? 'percent' : 'amount');
// محاسبه مالیات
$taxAmount = 0;
if ($preInvoice->getTaxPercent() && $preInvoice->getTaxPercent() > 0) {
$taxAmount = ($itemTotalAfterDiscount * $preInvoice->getTaxPercent()) / 100;
$hesabdariRow->setTax($taxAmount);
$sumTax += $taxAmount;
}
// تنظیم مرجع حسابداری (جدول 1 برای فروش)
$ref = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '1']);
$hesabdariRow->setRef($ref);
$this->entityManager->persist($hesabdariRow);
$sumTotal += $itemTotalAfterDiscount;
$totalItemDiscount += $itemDiscount;
}
// محاسبه تخفیف درصدی اگر مبلغ تخفیف صفر باشد اما درصد تخفیف وجود داشته باشد
if ($totalDiscountAmount == 0 && $preInvoice->getTotalDiscountPercent() && $preInvoice->getTotalDiscountPercent() > 0) {
$totalDiscountAmount = ($sumTotal * floatval($preInvoice->getTotalDiscountPercent())) / 100;
}
// افزودن ردیف مالیات کل
if ($sumTax > 0) {
$taxRow = new HesabdariRow();
$taxRow->setDes('مالیات بر ارزش افزوده');
$taxRow->setBid($acc['bid']);
$taxRow->setYear($acc['year']);
$taxRow->setDoc($sellDoc);
$taxRow->setBs($sumTax);
$taxRow->setBd(0);
$taxRef = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '33']);
$taxRow->setRef($taxRef);
$this->entityManager->persist($taxRow);
}
// افزودن ردیف تخفیف کل
if ($totalDiscountAmount > 0) {
$discountRow = new HesabdariRow();
$discountRow->setDes('تخفیف کل');
$discountRow->setBid($acc['bid']);
$discountRow->setYear($acc['year']);
$discountRow->setDoc($sellDoc);
$discountRow->setBs(0);
$discountRow->setBd($totalDiscountAmount);
$discountRow->setDiscount($totalDiscountAmount);
$discountRow->setDiscountPercent(floatval($preInvoice->getTotalDiscountPercent() ?: 0));
$discountRow->setDiscountType($preInvoice->isShowTotalPercentDiscount() ? 'percent' : 'amount');
// استفاده از جدول تخفیف (کد 104) - مطابق با Controller فاکتور فروش
$discountRef = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '104']);
if (!$discountRef) {
// اگر جدول تخفیف وجود نداشت، از جدول 4 استفاده کن
$discountRef = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '4']);
if (!$discountRef) {
// اگر جدول 4 هم وجود نداشت، از جدول 1 استفاده کن
$discountRef = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '1']);
}
}
$discountRow->setRef($discountRef);
$this->entityManager->persist($discountRow);
}
// افزودن ردیف هزینه حمل
if ($shippingCost > 0) {
$shippingRow = new HesabdariRow();
$shippingRow->setDes('هزینه حمل و نقل');
$shippingRow->setBid($acc['bid']);
$shippingRow->setYear($acc['year']);
$shippingRow->setDoc($sellDoc);
$shippingRow->setBs($shippingCost);
$shippingRow->setBd(0);
// استفاده از جدول درآمد حمل کالا (کد 61) - مطابق با Controller فاکتور فروش
$shippingRef = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '61']);
if (!$shippingRef) {
// اگر جدول درآمد حمل کالا وجود نداشت، از جدول هزینه حمل کالا (کد 90) استفاده کن
$shippingRef = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '90']);
if (!$shippingRef) {
// اگر جدول 90 هم وجود نداشت، از جدول 1 استفاده کن
$shippingRef = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '1']);
}
}
$shippingRow->setRef($shippingRef);
$this->entityManager->persist($shippingRow);
}
// افزودن ردیف اصلی فاکتور (بدهکار به مشتری)
$mainRow = new HesabdariRow();
$mainRow->setDes('فاکتور فروش');
$mainRow->setBid($acc['bid']);
$mainRow->setYear($acc['year']);
$mainRow->setDoc($sellDoc);
$mainRow->setBs(0);
$mainRow->setBd($sumTotal + $sumTax + $shippingCost - $totalDiscountAmount);
// تنظیم مشتری
$mainRow->setPerson($preInvoice->getPerson());
// تنظیم مرجع حسابداری (جدول 3 برای بدهی به مشتری)
$ref = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '3']);
$mainRow->setRef($ref);
$this->entityManager->persist($mainRow);
// به‌روزرسانی مبلغ کل فاکتور فروش
$finalAmount = $sumTotal + $sumTax + $shippingCost - $totalDiscountAmount;
$sellDoc->setAmount($finalAmount);
$this->entityManager->persist($sellDoc);
// ایجاد لینک کوتاه
$sellDoc->setShortlink($this->provider->RandomString(8));
// به‌روزرسانی وضعیت پیش‌فاکتور
$preInvoice->setStatus('converted');
$this->entityManager->persist($preInvoice);
$this->entityManager->flush();
// ثبت لاگ برای عملیات پیش‌فاکتور
$this->log->insertPreInvoiceLog(
'پیش‌فاکتور',
'تبدیل پیش‌فاکتور شماره ' . $preInvoice->getCode() . ' به فاکتور فروش شماره ' . $sellDoc->getCode(),
$user,
$acc['bid']->getId()
);
// ثبت لاگ برای فاکتور فروش ایجاد شده
$this->log->insert(
'فاکتور فروش',
'فاکتور فروش شماره ' . $sellDoc->getCode() . ' از پیش‌فاکتور شماره ' . $preInvoice->getCode() . ' ایجاد شد',
$user,
$acc['bid']->getId(),
$sellDoc
);
return [
'success' => true,
'message' => 'پیش‌فاکتور با موفقیت به فاکتور فروش تبدیل شد',
'invoiceCode' => $sellDoc->getCode(),
'invoiceId' => $sellDoc->getId()
];
} catch (\Exception $e) {
return [
'success' => false,
'message' => 'خطا در تبدیل پیش‌فاکتور: ' . $e->getMessage()
];
}
}
/**
* بررسی امکان تبدیل پیش‌فاکتور
*/
public function canConvert(PreInvoiceDoc $preInvoice): array
{
$errors = [];
// بررسی وضعیت پیش‌فاکتور
if ($preInvoice->getStatus() === 'converted') {
$errors[] = 'این پیش‌فاکتور قبلاً به فاکتور فروش تبدیل شده است';
}
// بررسی وجود آیتم‌ها
if ($preInvoice->getPreInvoiceItems()->count() === 0) {
$errors[] = 'پیش‌فاکتور باید حداقل یک آیتم داشته باشد';
}
// بررسی مشتری
if (!$preInvoice->getPerson()) {
$errors[] = 'مشتری برای پیش‌فاکتور تعیین نشده است';
}
return [
'canConvert' => empty($errors),
'errors' => $errors
];
}
}

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