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
238 changed files with 53516 additions and 3534 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

@ -16,6 +16,7 @@
"doctrine/orm": "^3.2",
"dompdf/dompdf": "^3.0",
"melipayamak/php": "^1.0",
"morilog/jalali": "*",
"mpdf/mpdf": "^8.2",
"nelmio/api-doc-bundle": "^4.35",
"nelmio/cors-bundle": "^2.5",

View file

@ -4,8 +4,75 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "43db0ad2bb94569ed6d44cabf503210e",
"content-hash": "01b5daf5a6fd011b4eb616e0e4ae18fe",
"packages": [
{
"name": "beberlei/assert",
"version": "v3.3.3",
"source": {
"type": "git",
"url": "https://github.com/beberlei/assert.git",
"reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/beberlei/assert/zipball/b5fd8eacd8915a1b627b8bfc027803f1939734dd",
"reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*",
"phpunit/phpunit": ">=6.0.0",
"yoast/phpunit-polyfills": "^0.1.0"
},
"suggest": {
"ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles"
},
"type": "library",
"autoload": {
"files": [
"lib/Assert/functions.php"
],
"psr-4": {
"Assert\\": "lib/Assert"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de",
"role": "Lead Developer"
},
{
"name": "Richard Quadling",
"email": "rquadling@gmail.com",
"role": "Collaborator"
}
],
"description": "Thin assertion library for input validation in business models.",
"keywords": [
"assert",
"assertion",
"validation"
],
"support": {
"issues": "https://github.com/beberlei/assert/issues",
"source": "https://github.com/beberlei/assert/tree/v3.3.3"
},
"time": "2024-07-15T13:18:35+00:00"
},
{
"name": "brick/math",
"version": "0.12.3",
@ -66,6 +133,75 @@
],
"time": "2025-02-28T13:11:00+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
"version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon-doctrine-types.git",
"reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d",
"reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"conflict": {
"doctrine/dbal": "<4.0.0 || >=5.0.0"
},
"require-dev": {
"doctrine/dbal": "^4.0.0",
"nesbot/carbon": "^2.71.0 || ^3.0.0",
"phpunit/phpunit": "^10.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "KyleKatarn",
"email": "kylekatarnls@gmail.com"
}
],
"description": "Types to use Carbon in Doctrine",
"keywords": [
"carbon",
"date",
"datetime",
"doctrine",
"time"
],
"support": {
"issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues",
"source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0"
},
"funding": [
{
"url": "https://github.com/kylekatarnls",
"type": "github"
},
{
"url": "https://opencollective.com/Carbon",
"type": "open_collective"
},
{
"url": "https://tidelift.com/funding/github/packagist/nesbot/carbon",
"type": "tidelift"
}
],
"time": "2024-02-09T16:56:22+00:00"
},
{
"name": "composer/pcre",
"version": "3.3.2",
@ -2297,6 +2433,71 @@
],
"time": "2025-03-24T10:02:05+00:00"
},
{
"name": "morilog/jalali",
"version": "v3.4.2",
"source": {
"type": "git",
"url": "https://github.com/morilog/jalali.git",
"reference": "f475f4db7bd540c6abc01126e46824c897ed1e03"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/morilog/jalali/zipball/f475f4db7bd540c6abc01126e46824c897ed1e03",
"reference": "f475f4db7bd540c6abc01126e46824c897ed1e03",
"shasum": ""
},
"require": {
"beberlei/assert": "^3.0",
"nesbot/carbon": "^1.21 || ^2.0 || ^3.0",
"php": "^7.0 | ^8.0"
},
"require-dev": {
"phpunit/phpunit": ">4.0"
},
"type": "library",
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Morilog\\Jalali\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Milad Rey",
"email": "miladr@gmail.com"
},
{
"name": "Morteza Parvini",
"email": "m.parvini@outlook.com"
}
],
"description": "This Package helps developers to easily work with Jalali (Shamsi or Iranian) dates in PHP applications, based on Jalali (Shamsi) DateTime class.",
"keywords": [
"Jalali",
"date",
"datetime",
"laravel",
"morilog"
],
"support": {
"issues": "https://github.com/morilog/jalali/issues",
"source": "https://github.com/morilog/jalali/tree/v3.4.2"
},
"funding": [
{
"url": "https://issuehunt.io/r/morilog",
"type": "issuehunt"
}
],
"time": "2024-05-09T08:44:51+00:00"
},
{
"name": "mpdf/mpdf",
"version": "v8.2.5",
@ -2714,6 +2915,111 @@
},
"time": "2024-06-24T21:25:28+00:00"
},
{
"name": "nesbot/carbon",
"version": "3.10.2",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
"reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24",
"reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24",
"shasum": ""
},
"require": {
"carbonphp/carbon-doctrine-types": "<100.0",
"ext-json": "*",
"php": "^8.1",
"psr/clock": "^1.0",
"symfony/clock": "^6.3.12 || ^7.0",
"symfony/polyfill-mbstring": "^1.0",
"symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
"doctrine/dbal": "^3.6.3 || ^4.0",
"doctrine/orm": "^2.15.2 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.75.0",
"kylekatarnls/multi-tester": "^2.5.3",
"phpmd/phpmd": "^2.15.0",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.17",
"phpunit/phpunit": "^10.5.46",
"squizlabs/php_codesniffer": "^3.13.0"
},
"bin": [
"bin/carbon"
],
"type": "library",
"extra": {
"laravel": {
"providers": [
"Carbon\\Laravel\\ServiceProvider"
]
},
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-2.x": "2.x-dev",
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Carbon\\": "src/Carbon/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "https://markido.com"
},
{
"name": "kylekatarnls",
"homepage": "https://github.com/kylekatarnls"
}
],
"description": "An API extension for DateTime that supports 281 different languages.",
"homepage": "https://carbon.nesbot.com",
"keywords": [
"date",
"datetime",
"time"
],
"support": {
"docs": "https://carbon.nesbot.com/docs",
"issues": "https://github.com/CarbonPHP/carbon/issues",
"source": "https://github.com/CarbonPHP/carbon"
},
"funding": [
{
"url": "https://github.com/sponsors/kylekatarnls",
"type": "github"
},
{
"url": "https://opencollective.com/Carbon#sponsor",
"type": "opencollective"
},
{
"url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme",
"type": "tidelift"
}
],
"time": "2025-08-02T09:36:06+00:00"
},
{
"name": "nikic/php-parser",
"version": "v5.4.0",

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

@ -5,6 +5,22 @@ parameters:
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:
autowire: true
@ -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

@ -1,40 +0,0 @@
<?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 Version20241220000000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add memberCount field to chat_channel table';
}
public function up(Schema $schema): void
{
// Add memberCount column to chat_channel table
$this->addSql('ALTER TABLE chat_channel ADD member_count INT NOT NULL DEFAULT 0');
// Update existing channels with correct member count
$this->addSql('
UPDATE chat_channel c
SET member_count = (
SELECT COUNT(*)
FROM chat_channel_member m
WHERE m.channel_id = c.id AND m.is_active = 1
)
');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chat_channel DROP member_count');
}
}

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,40 @@
<?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 Version20250113000000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add approval fields to HesabdariDoc and HesabdariRow tables';
}
public function up(Schema $schema): void
{
// Add approval fields to HesabdariDoc table
$this->addSql('ALTER TABLE hesabdari_doc ADD is_preview TINYINT(1) DEFAULT NULL');
$this->addSql('ALTER TABLE hesabdari_doc ADD is_approved TINYINT(1) DEFAULT NULL');
$this->addSql('ALTER TABLE hesabdari_doc ADD approved_by_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE hesabdari_doc ADD CONSTRAINT FK_HESABDARI_DOC_APPROVED_BY FOREIGN KEY (approved_by_id) REFERENCES user (id)');
$this->addSql('CREATE INDEX IDX_HESABDARI_DOC_APPROVED_BY ON hesabdari_doc (approved_by_id)');
// Set default values for existing documents
$this->addSql('UPDATE hesabdari_doc SET is_preview = 0, is_approved = 1 WHERE is_preview IS NULL');
}
public function down(Schema $schema): void
{
// Remove approval fields from HesabdariDoc table
$this->addSql('ALTER TABLE hesabdari_doc DROP FOREIGN KEY FK_HESABDARI_DOC_APPROVED_BY');
$this->addSql('DROP INDEX IDX_HESABDARI_DOC_APPROVED_BY ON hesabdari_doc');
$this->addSql('ALTER TABLE hesabdari_doc DROP is_preview, DROP is_approved, DROP approved_by_id');
}
}

View file

@ -0,0 +1,31 @@
<?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 Version20250113000001 extends AbstractMigration
{
public function getDescription(): string
{
return 'Remove status field from StoreroomTicket table';
}
public function up(Schema $schema): void
{
// Remove status field from storeroom_ticket table
$this->addSql('ALTER TABLE storeroom_ticket DROP status');
}
public function down(Schema $schema): void
{
// Add status field back to storeroom_ticket table
$this->addSql('ALTER TABLE storeroom_ticket ADD status VARCHAR(50) DEFAULT NULL');
}
}

View file

@ -0,0 +1,40 @@
<?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 Version20250113000002 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add approval fields to StoreroomTicket table';
}
public function up(Schema $schema): void
{
// Add approval fields to storeroom_ticket table
$this->addSql('ALTER TABLE storeroom_ticket ADD is_preview TINYINT(1) DEFAULT NULL');
$this->addSql('ALTER TABLE storeroom_ticket ADD is_approved TINYINT(1) DEFAULT NULL');
$this->addSql('ALTER TABLE storeroom_ticket ADD approved_by_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE storeroom_ticket ADD CONSTRAINT FK_STOREROOM_TICKET_APPROVED_BY FOREIGN KEY (approved_by_id) REFERENCES user (id)');
$this->addSql('CREATE INDEX IDX_STOREROOM_TICKET_APPROVED_BY ON storeroom_ticket (approved_by_id)');
// Set default values for existing tickets
$this->addSql('UPDATE storeroom_ticket SET is_preview = 0, is_approved = 1 WHERE is_preview IS NULL');
}
public function down(Schema $schema): void
{
// Remove approval fields from storeroom_ticket table
$this->addSql('ALTER TABLE storeroom_ticket DROP FOREIGN KEY FK_STOREROOM_TICKET_APPROVED_BY');
$this->addSql('DROP INDEX IDX_STOREROOM_TICKET_APPROVED_BY ON storeroom_ticket');
$this->addSql('ALTER TABLE storeroom_ticket DROP is_preview, DROP is_approved, DROP approved_by_id');
}
}

View file

@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250809100001 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create custom_invoice_template table';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE custom_invoice_template (id INT AUTO_INCREMENT NOT NULL, bid_id INT NOT NULL, submitter_id INT NOT NULL, name VARCHAR(255) NOT NULL, is_public TINYINT(1) NOT NULL, code LONGTEXT NOT NULL, INDEX IDX_CUSTOM_INV_TPL_BID (bid_id), INDEX IDX_CUSTOM_INV_TPL_SUBMITTER (submitter_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE custom_invoice_template ADD CONSTRAINT FK_CUSTOM_INV_TPL_BID FOREIGN KEY (bid_id) REFERENCES business (id)');
$this->addSql('ALTER TABLE custom_invoice_template ADD CONSTRAINT FK_CUSTOM_INV_TPL_SUBMITTER FOREIGN KEY (submitter_id) REFERENCES `user` (id)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE custom_invoice_template DROP FOREIGN KEY FK_CUSTOM_INV_TPL_BID');
$this->addSql('ALTER TABLE custom_invoice_template DROP FOREIGN KEY FK_CUSTOM_INV_TPL_SUBMITTER');
$this->addSql('DROP TABLE custom_invoice_template');
}
}

View file

@ -0,0 +1,31 @@
<?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 Version20250811093832 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
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
}
}

View file

@ -0,0 +1,31 @@
<?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 Version20250811101253 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
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250811120010 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add warranty usage columns to plug_warranty_serial table';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE plug_warranty_serial ADD used TINYINT(1) DEFAULT NULL');
$this->addSql('ALTER TABLE plug_warranty_serial ADD used_at VARCHAR(50) DEFAULT NULL');
$this->addSql('ALTER TABLE plug_warranty_serial ADD used_ticket_code VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE plug_warranty_serial DROP used');
$this->addSql('ALTER TABLE plug_warranty_serial DROP used_at');
$this->addSql('ALTER TABLE plug_warranty_serial DROP used_ticket_code');
}
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250811123020 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add status and importWorkflowCode to storeroom_ticket';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE storeroom_ticket ADD status VARCHAR(50) DEFAULT NULL');
$this->addSql('ALTER TABLE storeroom_ticket ADD import_workflow_code VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE storeroom_ticket DROP status');
$this->addSql('ALTER TABLE storeroom_ticket DROP import_workflow_code');
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250811124530 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add two-step approval flags to permission table';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE permission ADD require_two_step_sell TINYINT(1) DEFAULT NULL');
$this->addSql('ALTER TABLE permission ADD require_two_step_payment TINYINT(1) DEFAULT NULL');
$this->addSql('ALTER TABLE permission ADD require_two_step_store TINYINT(1) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE permission DROP require_two_step_sell');
$this->addSql('ALTER TABLE permission DROP require_two_step_payment');
$this->addSql('ALTER TABLE permission DROP require_two_step_store');
}
}

View file

@ -0,0 +1,101 @@
<?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 Version20250815143325 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 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);
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial DROP FOREIGN KEY FK_1A5DC26F4D9866B8
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_1A5DC26F4D9866B8 ON plug_warranty_serial
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial ADD allocated_to_document_id INT DEFAULT NULL, ADD allocated_at DATETIME DEFAULT NULL, ADD bound_to_item_id INT DEFAULT NULL, ADD bound_at DATETIME DEFAULT NULL, DROP used, DROP used_at, CHANGE date_submit date_submit DATETIME NOT NULL, CHANGE description description LONGTEXT DEFAULT NULL, CHANGE warranty_start_date warranty_start_date DATETIME DEFAULT NULL, CHANGE warranty_end_date warranty_end_date DATETIME DEFAULT NULL, CHANGE status status VARCHAR(20) NOT NULL, CHANGE notes notes LONGTEXT DEFAULT NULL, CHANGE used_ticket_code void_reason VARCHAR(255) DEFAULT NULL, CHANGE bid_id business_id INT NOT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial ADD CONSTRAINT FK_1A5DC26FA89DB457 FOREIGN KEY (business_id) REFERENCES business (id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_1A5DC26FA89DB457 ON plug_warranty_serial (business_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX idx_status_product ON plug_warranty_serial (status, commodity_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX idx_alloc_doc ON plug_warranty_serial (allocated_to_document_id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial RENAME INDEX uniq_1a5dc26fd948ee2 TO uniq_warranty_serial
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE storeroom_ticket RENAME INDEX idx_storeroom_ticket_approved_by TO IDX_9B4CC0F72D234F6A
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 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) ON UPDATE NO ACTION ON DELETE NO ACTION
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_83B2C6EC2D234F6A ON hesabdari_row (approved_by_id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial DROP FOREIGN KEY FK_1A5DC26FA89DB457
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_1A5DC26FA89DB457 ON plug_warranty_serial
SQL);
$this->addSql(<<<'SQL'
DROP INDEX idx_status_product ON plug_warranty_serial
SQL);
$this->addSql(<<<'SQL'
DROP INDEX idx_alloc_doc ON plug_warranty_serial
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial ADD used TINYINT(1) DEFAULT NULL, ADD used_at VARCHAR(50) DEFAULT NULL, DROP allocated_to_document_id, DROP allocated_at, DROP bound_to_item_id, DROP bound_at, CHANGE date_submit date_submit VARCHAR(25) NOT NULL, CHANGE description description VARCHAR(255) DEFAULT NULL, CHANGE warranty_start_date warranty_start_date VARCHAR(25) DEFAULT NULL, CHANGE warranty_end_date warranty_end_date VARCHAR(25) DEFAULT NULL, CHANGE status status VARCHAR(50) DEFAULT NULL, CHANGE notes notes VARCHAR(255) DEFAULT NULL, CHANGE business_id bid_id INT NOT NULL, CHANGE void_reason used_ticket_code VARCHAR(255) DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial ADD CONSTRAINT FK_1A5DC26F4D9866B8 FOREIGN KEY (bid_id) REFERENCES business (id) ON UPDATE NO ACTION ON DELETE NO ACTION
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_1A5DC26F4D9866B8 ON plug_warranty_serial (bid_id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial RENAME INDEX uniq_warranty_serial TO UNIQ_1A5DC26FD948EE2
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE storeroom_ticket RENAME INDEX idx_9b4cc0f72d234f6a TO IDX_STOREROOM_TICKET_APPROVED_BY
SQL);
}
}

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 Version20250816171207 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 commodity_serial VARCHAR(255) DEFAULT NULL, ADD buyer_id INT DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial ADD CONSTRAINT FK_1A5DC26F6C755722 FOREIGN KEY (buyer_id) REFERENCES person (id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_1A5DC26F6C755722 ON plug_warranty_serial (buyer_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_1A5DC26F6C755722
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_1A5DC26F6C755722 ON plug_warranty_serial
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE plug_warranty_serial DROP commodity_serial, DROP buyer_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 Version20250816185111 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 activation VARCHAR(20) 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 plug_warranty_serial DROP activation
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 Version20250816185556 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 activation_at VARCHAR(20) 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 plug_warranty_serial DROP activation_at
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 Version20250818042052 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 CHANGE activation_at activation_at DATETIME 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 plug_warranty_serial CHANGE activation_at activation_at VARCHAR(20) 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 Version20250818042232 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 CHANGE activation_at activation_at DATETIME 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 plug_warranty_serial CHANGE activation_at activation_at VARCHAR(20) 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 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,23 @@ 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'] != '') {
$prelabel = $em->getRepository(\App\Entity\PersonPrelabel::class)->findOneBy(['label' => $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
{
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';
}
}
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
]);
}
}

View file

@ -0,0 +1,979 @@
<?php
/**
* Developed by Mohammad Rezai
* https://pirouz.xyz 2025-08-24
*/
namespace App\Controller;
use App\Entity\Business;
use App\Entity\HesabdariDoc;
use App\Entity\HesabdariRow;
use App\Entity\Log;
use App\Entity\Permission;
use App\Entity\User;
use App\Entity\Year;
use App\Service\Access;
use App\Service\Log as LogService;
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\Http\Attribute\CurrentUser;
use OpenApi\Annotations as OA;
class ApprovalController extends AbstractController
{
/**
* تأیید حواله انبار
*
* @OA\Post(
* path="/api/approval/approve/storeroom/{ticketCode}",
* summary="تأیید حواله انبار",
* tags={"Approval"},
* @OA\Parameter(
* name="ticketCode",
* 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="message", type="string", example="حواله انبار با موفقیت تأیید شد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز یا تأیید دو مرحله‌ای فعال نیست"),
* @OA\Response(response=404, description="حواله انبار یافت نشد"),
* @OA\Response(response=500, description="خطا در تأیید حواله انبار")
* )
*/
#[Route('/api/approval/approve/storeroom/{ticketCode}', name: 'api_approval_approve_storeroom', methods: ['POST'])]
public function approveStoreroomTicket(
$ticketCode,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('store');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$ticket = $entityManager->getRepository(\App\Entity\StoreroomTicket::class)->findOneBy([
'code' => $ticketCode,
'bid' => $business
]);
if (!$ticket) {
return $this->json(['success' => false, 'message' => 'حواله انبار یافت نشد']);
}
$canApprove = $this->canUserApproveDocument($user, $businessSettings, 'storeroom');
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']);
}
if (!$this->checkDocumentYear($ticket->getDoc(), $business, $entityManager)) {
return $this->json(['success' => false, 'message' => 'حواله مربوط به این سال مالی نیست']);
}
$ticket->setIsPreview(false);
$ticket->setIsApproved(true);
$ticket->setApprovedBy($user);
$entityManager->persist($ticket);
$entityManager->flush();
$logService->insert(
'تأیید حواله انبار',
"حواله انبار {$ticket->getCode()} توسط {$user->getFullName()} تأیید شد",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'حواله انبار با موفقیت تأیید شد'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در تأیید حواله انبار: ' . $e->getMessage()
], 500);
}
}
/**
* لغو تأیید حواله انبار
*
* @OA\Post(
* path="/api/approval/unapprove/storeroom/{ticketCode}",
* summary="لغو تأیید حواله انبار",
* tags={"Approval"},
* @OA\Parameter(
* name="ticketCode",
* 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="message", type="string", example="حواله انبار با موفقیت لغو تأیید شد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز یا تأیید دو مرحله‌ای فعال نیست"),
* @OA\Response(response=404, description="حواله انبار یافت نشد"),
* @OA\Response(response=500, description="خطا در لغو تأیید حواله انبار")
* )
*/
#[Route('/api/approval/unapprove/storeroom/{ticketCode}', name: 'api_approval_unapprove_storeroom', methods: ['POST'])]
public function unapproveStoreroomTicket(
$ticketCode,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('store');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$ticket = $entityManager->getRepository(\App\Entity\StoreroomTicket::class)->findOneBy([
'code' => $ticketCode,
'bid' => $business
]);
if (!$ticket) {
return $this->json(['success' => false, 'message' => 'حواله انبار یافت نشد']);
}
$canApprove = $this->canUserApproveDocument($user, $businessSettings, 'storeroom');
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']);
}
if (!$this->checkDocumentYear($ticket->getDoc(), $business, $entityManager)) {
return $this->json(['success' => false, 'message' => 'حواله مربوط به این سال مالی نیست']);
}
$ticket->setIsPreview(true);
$ticket->setIsApproved(false);
$ticket->setApprovedBy(null);
$entityManager->persist($ticket);
$entityManager->flush();
$logService->insert(
'لغو تأیید حواله انبار',
"حواله انبار {$ticket->getCode()} توسط {$user->getFullName()} لغو تأیید شد",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'حواله انبار با موفقیت لغو تأیید شد'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در لغو تأیید حواله انبار: ' . $e->getMessage()
], 500);
}
}
/**
* تأیید فاکتور فروش
*
* @OA\Post(
* path="/api/approval/approve/sales/{docId}",
* summary="تأیید فاکتور فروش",
* tags={"Approval"},
* @OA\Parameter(
* name="docId",
* 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="message", type="string", example="فاکتور فروش با موفقیت تأیید شد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز یا تأیید دو مرحله‌ای فعال نیست"),
* @OA\Response(response=404, description="فاکتور فروش یافت نشد"),
* @OA\Response(response=500, description="خطا در تأیید فاکتور فروش")
* )
*/
#[Route('/api/approval/approve/sales/{docId}', name: 'api_approval_approve_sales', methods: ['POST'])]
public function approveSalesInvoice(
$docId,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'code' => $docId,
'bid' => $business
]);
if (!$document) {
return $this->json(['success' => false, 'message' => 'فاکتور فروش یافت نشد']);
}
$canApprove = $this->canUserApproveDocument($user, $businessSettings, 'sell');
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
}
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
return $this->json(['success' => false, 'message' => 'فاکتور مربوط به این سال مالی نیست']);
}
$document->setIsPreview(false);
$document->setIsApproved(true);
$document->setApprovedBy($user);
$payments = [];
foreach ($document->getRelatedDocs() as $relatedDoc) {
if ($relatedDoc->getType() === 'sell_receive') {
$payments[] = $relatedDoc;
}
}
foreach ($payments as $payment) {
$payment->setIsPreview(false);
$payment->setIsApproved(true);
$payment->setApprovedBy($user);
$entityManager->persist($payment);
}
$entityManager->persist($document);
$entityManager->flush();
$logService->insert(
'تأیید فاکتور فروش',
"فاکتور فروش {$document->getCode()} توسط {$user->getFullName()} تأیید شد",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'فاکتور فروش با موفقیت تأیید شد'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در تأیید فاکتور فروش: ' . $e->getMessage()
], 500);
}
}
/**
* تأیید گروهی فاکتورهای فروش
*
* @OA\Post(
* path="/api/approval/approve/group/sales",
* summary="تأیید گروهی فاکتورهای فروش",
* tags={"Approval"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"docIds"},
* @OA\Property(property="docIds", 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", example="فاکتورهای فروش با موفقیت تأیید شدند")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز یا تأیید دو مرحله‌ای فعال نیست"),
* @OA\Response(response=404, description="فاکتور فروش یافت نشد"),
* @OA\Response(response=500, description="خطا در تأیید فاکتورهای فروش")
* )
*/
#[Route('/api/approval/approve/group/sales', name: 'api_approval_approve_group_sales', methods: ['POST'])]
public function approveSalesInvoiceGroup(
Request $request,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$canApprove = $this->canUserApproveDocument($user, $businessSettings, 'sell');
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتورها را ندارید']);
}
$data = json_decode($request->getContent(), true);
$docIds = $data['docIds'] ?? [];
foreach ($docIds as $docId) {
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'code' => $docId,
'bid' => $business
]);
if (!$document) {
return $this->json(['success' => false, 'message' => 'فاکتور فروش یافت نشد']);
}
if ($document->isApproved()) {
return $this->json(['success' => false, 'message' => 'فاکتور فروش تایید شده است']);
}
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
return $this->json(['success' => false, 'message' => 'فاکتور مربوط به این سال مالی نیست']);
}
$document->setIsPreview(false);
$document->setIsApproved(true);
$document->setApprovedBy($user);
$payments = [];
foreach ($document->getRelatedDocs() as $relatedDoc) {
if ($relatedDoc->getType() === 'sell_receive') {
$payments[] = $relatedDoc;
}
}
foreach ($payments as $payment) {
$payment->setIsPreview(false);
$payment->setIsApproved(true);
$payment->setApprovedBy($user);
$entityManager->persist($payment);
}
$entityManager->persist($document);
$entityManager->flush();
}
$logService->insert(
'تأیید فاکتورهای فروش',
"فاکتورهای فروش {$docIds} توسط {$user->getFullName()} تأیید شدند",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'فاکتورهای فروش با موفقیت تأیید شدند'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در تأیید فاکتورهای فروش: ' . $e->getMessage()
], 500);
}
}
/**
* لغو تأیید فاکتور فروش
*
* @OA\Post(
* path="/api/approval/unapprove/sales/{docId}",
* summary="لغو تأیید فاکتور فروش",
* tags={"Approval"},
* @OA\Parameter(
* name="docId",
* 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="message", type="string", example="فاکتور فروش با موفقیت لغو تأیید شد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز یا تأیید دو مرحله‌ای فعال نیست"),
* @OA\Response(response=404, description="فاکتور فروش یافت نشد"),
* @OA\Response(response=500, description="خطا در لغو تأیید فاکتور فروش")
* )
*/
#[Route('/api/approval/unapprove/sales/{docId}', name: 'api_approval_unapprove_sales', methods: ['POST'])]
public function unapproveSalesInvoice(
$docId,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'code' => $docId,
'bid' => $business
]);
if (!$document) {
return $this->json(['success' => false, 'message' => 'فاکتور فروش یافت نشد']);
}
$canApprove = $this->canUserApproveDocument($user, $businessSettings, 'sell');
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
}
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
return $this->json(['success' => false, 'message' => 'فاکتور مربوط به این سال مالی نیست']);
}
$document->setIsPreview(true);
$document->setIsApproved(false);
$document->setApprovedBy(null);
$payments = [];
foreach ($document->getRelatedDocs() as $relatedDoc) {
if ($relatedDoc->getType() === 'sell_receive') {
$payments[] = $relatedDoc;
}
}
foreach ($payments as $payment) {
$payment->setIsPreview(true);
$payment->setIsApproved(false);
$payment->setApprovedBy(null);
$entityManager->persist($payment);
}
$entityManager->persist($document);
$entityManager->flush();
$logService->insert(
'لغو تأیید فاکتور فروش',
"فاکتور فروش {$document->getCode()} توسط {$user->getFullName()} لغو تأیید شد",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'فاکتور فروش با موفقیت لغو تأیید شد'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در لغو تأیید فاکتور فروش: ' . $e->getMessage()
], 500);
}
}
/**
* تأیید فاکتور خرید
*
* @OA\Post(
* path="/api/approval/approve/buy/{docId}",
* summary="تأیید فاکتور خرید",
* tags={"Approval"},
* @OA\Parameter(
* name="docId",
* 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="message", type="string", example="فاکتور خرید با موفقیت تأیید شد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز یا تأیید دو مرحله‌ای فعال نیست"),
* @OA\Response(response=404, description="فاکتور خرید یافت نشد"),
* @OA\Response(response=500, description="خطا در تأیید فاکتور خرید")
* )
*/
#[Route('/api/approval/approve/buy/{docId}', name: 'api_approval_approve_buy', methods: ['POST'])]
public function approveBuyInvoice(
$docId,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('buy');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'code' => $docId,
'bid' => $business
]);
if (!$document) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید یافت نشد']);
}
$canApprove = $this->canUserApproveDocument($user, $businessSettings, 'buy');
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
}
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']);
}
$document->setIsPreview(false);
$document->setIsApproved(true);
$document->setApprovedBy($user);
$entityManager->persist($document);
$entityManager->flush();
$logService->insert(
'تأیید فاکتور خرید',
"فاکتور خرید {$document->getCode()} توسط {$user->getFullName()} تأیید شد",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'فاکتور خرید با موفقیت تأیید شد'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در تأیید فاکتور خرید: ' . $e->getMessage()
], 500);
}
}
/**
* تأیید گروهی فاکتورهای خرید
*
* @OA\Post(
* path="/api/approval/approve/group/buy",
* summary="تأیید گروهی فاکتورهای خرید",
* tags={"Approval"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"docIds"},
* @OA\Property(property="docIds", 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", example="فاکتورهای خرید با موفقیت تأیید شدند")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز یا تأیید دو مرحله‌ای فعال نیست"),
* @OA\Response(response=404, description="فاکتور خرید یافت نشد"),
* @OA\Response(response=500, description="خطا در تأیید فاکتورهای خرید")
* )
*/
#[Route('/api/approval/approve/group/buy', name: 'api_approval_approve_group_buy', methods: ['POST'])]
public function approveBuyInvoiceGroup(
Request $request,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('buy');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$canApprove = $this->canUserApproveDocument($user, $businessSettings, 'buy');
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتورها را ندارید']);
}
$data = json_decode($request->getContent(), true);
$docIds = $data['docIds'] ?? [];
foreach ($docIds as $docId) {
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'code' => $docId,
'bid' => $business
]);
if (!$document) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید یافت نشد']);
}
if ($document->isApproved()) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید تایید شده است']);
}
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']);
}
$document->setIsPreview(false);
$document->setIsApproved(true);
$document->setApprovedBy($user);
$entityManager->persist($document);
}
$entityManager->flush();
$logService->insert(
'تأیید فاکتورهای خرید',
"فاکتورهای خرید " . implode(', ', $docIds) . " توسط {$user->getFullName()} تأیید شدند",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'فاکتورهای خرید با موفقیت تأیید شدند'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در تأیید فاکتورهای خرید: ' . $e->getMessage()
], 500);
}
}
/**
* لغو تأیید فاکتور خرید
*
* @OA\Post(
* path="/api/approval/unapprove/buy/{docId}",
* summary="لغو تأیید فاکتور خرید",
* tags={"Approval"},
* @OA\Parameter(
* name="docId",
* 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="message", type="string", example="فاکتور خرید با موفقیت لغو تأیید شد")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز یا تأیید دو مرحله‌ای فعال نیست"),
* @OA\Response(response=404, description="فاکتور خرید یافت نشد"),
* @OA\Response(response=500, description="خطا در لغو تأیید فاکتور خرید")
* )
*/
#[Route('/api/approval/unapprove/buy/{docId}', name: 'api_approval_unapprove_buy', methods: ['POST'])]
public function unapproveBuyInvoice(
$docId,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('buy');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'code' => $docId,
'bid' => $business
]);
if (!$document) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید یافت نشد']);
}
$canApprove = $this->canUserApproveDocument($user, $businessSettings, 'buy');
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
}
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']);
}
$document->setIsPreview(true);
$document->setIsApproved(false);
$document->setApprovedBy(null);
$entityManager->persist($document);
$entityManager->flush();
$logService->insert(
'لغو تأیید فاکتور خرید',
"فاکتور خرید {$document->getCode()} توسط {$user->getFullName()} لغو تأیید شد",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'فاکتور خرید با موفقیت لغو تأیید شد'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در لغو تأیید فاکتور خرید: ' . $e->getMessage()
], 500);
}
}
/**
* لغو تأیید گروهی فاکتورهای خرید
*
* @OA\Post(
* path="/api/approval/unapprove/group/buy",
* summary="لغو تأیید گروهی فاکتورهای خرید",
* tags={"Approval"},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"docIds"},
* @OA\Property(property="docIds", 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", example="فاکتورهای خرید با موفقیت لغو تأیید شدند")
* )
* ),
* @OA\Response(response=403, description="دسترسی غیرمجاز یا تأیید دو مرحله‌ای فعال نیست"),
* @OA\Response(response=404, description="فاکتور خرید یافت نشد"),
* @OA\Response(response=500, description="خطا در لغو تأیید فاکتورهای خرید")
* )
*/
#[Route('/api/approval/unapprove/group/buy', name: 'api_approval_unapprove_group_buy', methods: ['POST'])]
public function unapproveBuyInvoiceGroup(
Request $request,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('buy');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$canApprove = $this->canUserApproveDocument($user, $businessSettings, 'buy');
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتورها را ندارید']);
}
$data = json_decode($request->getContent(), true);
$docIds = $data['docIds'] ?? [];
foreach ($docIds as $docId) {
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'code' => $docId,
'bid' => $business
]);
if (!$document) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید یافت نشد']);
}
if (!$document->isApproved()) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید تایید نشده است']);
}
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']);
}
$document->setIsPreview(true);
$document->setIsApproved(false);
$document->setApprovedBy(null);
$entityManager->persist($document);
}
$entityManager->flush();
$logService->insert(
'لغو تأیید فاکتورهای خرید',
"فاکتورهای خرید " . implode(', ', $docIds) . " توسط {$user->getFullName()} لغو تأیید شدند",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'فاکتورهای خرید با موفقیت لغو تأیید شدند'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در لغو تأیید فاکتورهای خرید: ' . $e->getMessage()
], 500);
}
}
private function checkDocumentYear(HesabdariDoc $document, Business $business, EntityManagerInterface $entityManager): bool
{
$year = $entityManager->getRepository(Year::class)->findOneBy([
'bid' => $business,
'head' => true
]);
return $document->getYear()->getId() == $year->getId();
}
private function canUserApproveDocument(User $user, Business $business, string $documentType): bool
{
if ($user->getEmail() === $business->getOwner()->getEmail()) {
return true;
}
$approversMap = [
'getApproverSellInvoice' => ['sell'],
'getApproverBuyInvoice' => ['buy'],
'getApproverWarehouseTransfer' => ['storeroom'],
'getApproverReturnSell' => ['rfsell'],
'getApproverReturnBuy' => ['rfbuy'],
'getApproverReceiveFromPersons' => ['person_receive'],
'getApproverPayToPersons' => ['person_send'],
'getApproverAccountingDocs' => ['calc'],
'getApproverBankTransfers' => ['transfer'],
];
foreach ($approversMap as $method => $types) {
if (in_array($documentType, $types, true)) {
return $business->$method() === $user->getEmail();
}
}
return false;
}
}

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,10 +425,13 @@ class BankController extends AbstractController
'year' => $acc['year']
]);
if ($prs) {
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $prs->getDoc()->isApproved()) {
$transactions[] = $prs;
}
}
}
}
$spreadsheet = new Spreadsheet();
$activeWorksheet = $spreadsheet->getActiveSheet();
$arrayEntity = [
@ -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,10 +514,13 @@ class BankController extends AbstractController
'year'=>$acc['year']
]);
if ($prs) {
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $prs->getDoc()->isApproved()) {
$transactions[] = $prs;
}
}
}
}
$pid = $provider->createPrint(
$acc['bid'],
$this->getUser(),

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')]
@ -247,6 +261,26 @@ class BusinessController extends AbstractController
}
}
// Approval settings
$business->setRequireTwoStepApproval((bool)$params['requireTwoStepApproval'] ?? false);
$approvers = $params['approvers'] ?? [];
$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) {
return $this->json(['result' => 2]);
@ -261,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());
@ -546,8 +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 = [
@ -593,8 +635,13 @@ 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()
];
}
return $this->json($result);
@ -666,8 +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);
@ -707,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([
@ -721,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;
@ -744,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;
@ -766,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;
@ -788,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;
// محاسبه پرداختی‌ها
$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();
$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());
}
}
$temp['relatedDocsCount'] = count($item->getRelatedDocs());
$pays = 0;
foreach ($item->getRelatedDocs() as $relatedDoc) {
$pays += $relatedDoc->getAmount();
$dataTemp[] = $item;
}
$temp['relatedDocsPays'] = $pays;
$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());
}
}
if (!array_key_exists('discountAll', $temp))
$temp['discountAll'] = 0;
if (!array_key_exists('transferCost', $temp))
$temp['transferCost'] = 0;
$dataTemp[] = $temp;
}
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,10 +374,13 @@ class CashdeskController extends AbstractController
'year' => $acc['year']
]);
if ($prs) {
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $prs->getDoc()->isApproved()) {
$transactions[] = $prs;
}
}
}
}
$spreadsheet = new Spreadsheet();
$activeWorksheet = $spreadsheet->getActiveSheet();
$arrayEntity = [
@ -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,10 +470,13 @@ class CashdeskController extends AbstractController
'year'=>$acc['year']
]);
if ($prs) {
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $prs->getDoc()->isApproved()) {
$transactions[] = $prs;
}
}
}
}
$pid = $provider->createPrint(
$acc['bid'],
$this->getUser(),

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,10 +428,13 @@ class CostController extends AbstractController
'money' => $acc['money']
]);
if ($doc) {
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $doc->isApproved()) {
$items[] = $doc;
}
}
}
}
$pid = $provider->createPrint(
$acc['bid'],
@ -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,10 +498,13 @@ class CostController extends AbstractController
'money' => $acc['money']
]);
if ($doc) {
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $doc->isApproved()) {
$items[] = $doc;
}
}
}
}
// ایجاد فایل اکسل
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
@ -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'
];
$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();
}
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 = '';
$printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid' => $bid]);
if ($printSettings) {
$note = $printSettings->getSellNoteString();
}
$pdfPid = $provider->createPrint(
$bid,
$bid->getOwner(),
$this->renderView('pdf/printers/sell.html.twig', [
'bid' => $bid,
'doc' => $doc,
'rows' => $doc->getHesabdariRows(),
'person' => $person,
// 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
]),
'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();
// 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;
}
}

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,10 +425,13 @@ class IncomeController extends AbstractController
'money' => $acc['money']
]);
if ($doc) {
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $doc->isApproved()) {
$items[] = $doc;
}
}
}
}
$pid = $provider->createPrint(
$acc['bid'],
@ -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,10 +495,13 @@ class IncomeController extends AbstractController
'money' => $acc['money']
]);
if ($doc) {
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $doc->isApproved()) {
$items[] = $doc;
}
}
}
}
// ایجاد فایل اکسل
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
@ -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);
@ -479,15 +497,33 @@ class PluginController extends AbstractController
'icon' => ' taxplugin.jpg',
'defaultOn' => null,
],
// [
// 'name' => 'مدیریت گارانتی',
// 'code' => 'warranty',
// 'timestamp' => '32104000',
// 'timelabel' => 'یک سال',
// 'price' => '200000',
// 'icon' => 'warranty.png',
// 'defaultOn' => null,
// ],
[
'name' => 'مدیریت گارانتی',
'code' => 'warranty',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '200000',
'icon' => 'warranty.png',
'defaultOn' => null,
],
[
'name' => 'مدیریت واردات کالا',
'code' => 'import-workflow',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '200000',
'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

File diff suppressed because it is too large Load diff

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,6 +22,8 @@ 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
@ -32,6 +35,65 @@ class TaxSettingsController extends AbstractController
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
{
@ -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,
@ -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([
@ -843,7 +1057,7 @@ class TaxSettingsController extends AbstractController
throw new \Exception($validationResult['message']);
}
$invoiceDto = $this->buildInvoiceDto($invoice, $moadian, $taxSettings->getEconomicCode());
$invoiceDto = $this->buildInvoiceDto($invoice, $moadian, $taxSettings['economicCode']);
if (!$invoiceDto) {
throw new \Exception('خطا در آماده‌سازی فاکتور: خطا در ساخت DTO فاکتور');
}
@ -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 = '';
@ -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)
@ -1244,10 +1511,20 @@ class TaxSettingsController extends AbstractController
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([
@ -1443,7 +1751,7 @@ class TaxSettingsController extends AbstractController
continue;
}
$invoiceDto = $this->buildInvoiceDto($invoice, $moadian, $taxSettings->getEconomicCode());
$invoiceDto = $this->buildInvoiceDto($invoice, $moadian, $taxSettings['economicCode']);
if (!$invoiceDto) {
$results[] = [
'id' => $id,
@ -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' => 'تنظیمات مالیاتی تکمیل نشده است. لطفاً ابتدا تنظیمات را تکمیل کنید.'
@ -1611,7 +1955,6 @@ class TaxSettingsController extends AbstractController
$buyerEconomicCode = null;
$missingFields = [];
// دریافت شخص خریدار از ردیف‌های فاکتور
foreach ($invoice->getHesabdariRows() as $row) {
if ($row->getPerson()) {
$buyerPerson = $row->getPerson();

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

@ -0,0 +1,400 @@
<?php
namespace App\Controller;
use App\Entity\Business;
use App\Entity\PlugWarrantySerial;
use App\Service\PluginService;
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;
use Morilog\Jalali\CalendarUtils;
use App\Service\registryMGR;
class PublicController extends AbstractController
{
/**
* Check if warranty plugin is active for the business
*/
private function checkWarrantyPluginActive(Business $business, PluginService $pluginService): JsonResponse|null
{
if (!$pluginService->isActive('warranty', $business)) {
return $this->json([
'success' => false,
'message' => 'پلاگین گارانتی برای این کسب و کار فعال نیست',
'error' => 'PLUGIN_NOT_ACTIVE'
], 404);
}
return null;
}
#[Route('/api/public/{businessId}/warranty/check/{code}', name: 'api_public_warranty_check', methods: ['GET'])]
public function checkWarrantyCode(string $businessId, string $code, EntityManagerInterface $entityManager, PluginService $pluginService, registryMGR $registryMGR): JsonResponse
{
// Validate input
if (empty($code)) {
return $this->json([
'success' => false,
'message' => 'کد گارانتی وارد نشده است'
], 400);
}
// Find business
$business = $entityManager->getRepository(Business::class)->find($businessId);
if (!$business) {
return $this->json([
'success' => false,
'message' => 'کسب و کار مورد نظر یافت نشد'
], 404);
}
// Check if warranty plugin is active
$pluginCheck = $this->checkWarrantyPluginActive($business, $pluginService);
if ($pluginCheck) {
return $pluginCheck;
}
// Find warranty serial
$warrantySerial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
'business' => $business,
'serialNumber' => $code
]);
if (!$warrantySerial) {
return $this->json([
'success' => false,
'message' => 'کد گارانتی نامعتبر است یا متعلق به این کسب و کار نیست'
], 404);
}
// Check if already activated (used)
if (!$warrantySerial->isUsed()) {
return $this->json([
'success' => false,
'message' => 'این گارانتی ثبت نشده است',
], 400);
}
// if ($warrantySerial->getActivation() === 'active') {
// return $this->json([
// 'success' => false,
// 'message' => 'این گارانتی قبلاً فعال شده است',
// ], 400);
// }
$activationTimeLimit = (int) ($registryMGR->get('warranty', 'activationGraceDays') ?? 7);
$allocatedAt = $warrantySerial->getAllocatedAt();
$currentDate = new \DateTime();
$daysDiff = $currentDate->diff($allocatedAt)->days;
if ($daysDiff > $activationTimeLimit) {
return $this->json([
'success' => false,
'message' => "مهلت فعال‌سازی این گارانتی ({$activationTimeLimit} روز) به پایان رسیده است"
], 400);
}
// Get commodity information
$commodity = $warrantySerial->getCommodity();
// Calculate warranty period end date
$warrantyEndDate = null;
if ($warrantySerial->getWarrantyEndDate()) {
$warrantyEndDate = $warrantySerial->getWarrantyEndDate();
} elseif ($warrantySerial->getWarrantyStartDate()) {
// If only start date is set, assume 12 months warranty period
$startDate = new \DateTime($warrantySerial->getWarrantyStartDate());
$endDate = $startDate->add(new \DateInterval('P12M'));
$warrantyEndDate = $endDate->format('Y-m-d');
}
// Prepare product information
$productInfo = [
'serialNumber' => $warrantySerial->getSerialNumber(),
'commoditySerial' => $warrantySerial->getCommoditySerial(),
'productName' => $commodity ? $commodity->getName() : 'نامشخص',
'productCode' => $commodity ? $commodity->getCode() : null,
'description' => $warrantySerial->getDescription(),
'submitDate' => $warrantySerial->getDateSubmit(),
'warrantyStartDate' => $warrantySerial->getWarrantyStartDate(),
'warrantyEndDate' => $warrantyEndDate,
'status' => $warrantySerial->getStatus(),
'activation' => $warrantySerial->getActivation(),
'notes' => $warrantySerial->getNotes(),
'activationTimeLimit' => $activationTimeLimit,
'daysRemaining' => max(0, $activationTimeLimit - $daysDiff),
'submitter' => $warrantySerial->getSubmitter() ? $warrantySerial->getSubmitter()->getFullName() : null,
'businessName' => $business->getName(),
'activationTicketCode' => $warrantySerial->getActivationTicketCode(),
'requireActivationSecret' => true
];
return $this->json([
'success' => true,
'data' => $productInfo
]);
}
#[Route('/api/public/{businessId}/warranty/activate/{code}', name: 'api_public_warranty_activate', methods: ['POST'])]
public function activateWarranty(string $businessId, string $code, Request $request, EntityManagerInterface $entityManager, PluginService $pluginService, registryMGR $registryMGR): JsonResponse
{
// Validate input
if (empty($code)) {
return $this->json([
'success' => false,
'message' => 'کد گارانتی وارد نشده است'
], 400);
}
// Find business
$business = $entityManager->getRepository(Business::class)->find($businessId);
if (!$business) {
return $this->json([
'success' => false,
'message' => 'کسب و کار مورد نظر یافت نشد'
], 404);
}
// Check if warranty plugin is active
$pluginCheck = $this->checkWarrantyPluginActive($business, $pluginService);
if ($pluginCheck) {
return $pluginCheck;
}
// Find warranty serial
$warrantySerial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
'business' => $business,
'serialNumber' => $code
]);
if (!$warrantySerial) {
return $this->json([
'success' => false,
'message' => 'کد گارانتی نامعتبر است یا متعلق به این کسب و کار نیست'
], 404);
}
// Check if already activated
if (!$warrantySerial->isUsed()) {
return $this->json([
'success' => false,
'message' => 'این گارانتی ثبت نشده است',
], 400);
}
// Check status
if ($warrantySerial->getActivation() !== 'deactive') {
return $this->json([
'success' => false,
'message' => 'وضعیت این گارانتی فعال است'
], 400);
}
$activationTimeLimit = (int) ($registryMGR->get('warranty', 'activationGraceDays') ?? 7);
$allocatedAt = $warrantySerial->getAllocatedAt();
$currentDate = new \DateTime();
$daysDiff = $currentDate->diff($allocatedAt)->days;
if ($daysDiff > $activationTimeLimit) {
return $this->json([
'success' => false,
'message' => "مهلت فعال‌سازی این گارانتی ({$activationTimeLimit} روز) به پایان رسیده است"
], 400);
}
$secret = json_decode($request->getContent() ?: '{}', true)['activationSecret'] ?? '';
if ($warrantySerial->getActivationTicketSecret()) {
if (!$secret || $secret !== $warrantySerial->getActivationTicketSecret()) {
return $this->json([
'success' => false,
'message' => 'کد فعال‌سازی حواله نامعتبر است'
], 400);
}
}
$warrantySerial->setActivation('active');
$warrantySerial->setActivationAt(new \DateTimeImmutable());
// Set warranty start date to current date if not already set
// if (!$warrantySerial->getWarrantyStartDate()) {
// $warrantySerial->setWarrantyStartDate(date('Y-m-d'));
// }
// Set warranty end date if not already set (default 12 months)
// if (!$warrantySerial->getWarrantyEndDate()) {
// $endDate = new \DateTime();
// $endDate->add(new \DateInterval('P12M'));
// $warrantySerial->setWarrantyEndDate($endDate->format('Y-m-d'));
// }
// Save changes
$entityManager->persist($warrantySerial);
$entityManager->flush();
// Get commodity information for response
$commodity = $warrantySerial->getCommodity();
// Prepare activation information
$activationInfo = [
'serialNumber' => $warrantySerial->getSerialNumber(),
'productName' => $commodity ? $commodity->getName() : 'نامشخص',
'productCode' => $commodity ? $commodity->getCode() : null,
'activationDate' => $warrantySerial->getActivationAt()?->format('Y-m-d'),
'warrantyStartDate' => $warrantySerial->getWarrantyStartDate() ? jalaliToGregorian($warrantySerial->getWarrantyStartDate()->format('Y/m/d')) : null,
'warrantyEndDate' => $warrantySerial->getWarrantyEndDate() ? jalaliToGregorian($warrantySerial->getWarrantyEndDate()->format('Y/m/d')) : null,
'businessName' => $business->getName(),
'notes' => $warrantySerial->getNotes()
];
return $this->json([
'success' => true,
'message' => 'گارانتی با موفقیت فعال شد',
'data' => $activationInfo
]);
}
#[Route('/api/public/{businessId}/warranty/help', name: 'api_public_warranty_help', methods: ['GET'])]
public function getWarrantyHelp(string $businessId, PluginService $pluginService, EntityManagerInterface $entityManager): JsonResponse
{
// Find business
$business = $entityManager->getRepository(Business::class)->find($businessId);
if (!$business) {
return $this->json([
'success' => false,
'message' => 'کسب و کار مورد نظر یافت نشد'
], 404);
}
// Check if warranty plugin is active
$pluginCheck = $this->checkWarrantyPluginActive($business, $pluginService);
if ($pluginCheck) {
return $pluginCheck;
}
$helpInfo = [
'codeFormat' => [
'prefix' => 'WR',
'length' => 11,
'example' => 'WR-123456789',
'description' => 'کد گارانتی با حروف WR شروع می‌شود و شامل 9 رقم است'
],
'locations' => [
'محصول' => 'روی برچسب چسبیده شده به محصول',
'فاکتور' => 'در فاکتور خرید یا رسید پرداخت',
'بسته‌بندی' => 'در جعبه یا بسته‌بندی محصول',
'ایمیل' => 'در ایمیل تأیید خرید'
],
'activationTimeLimit' => [
'default' => 7,
'unit' => 'روز',
'description' => 'گارانتی باید حداکثر 7 روز پس از خرید فعال شود'
],
'support' => [
'phone' => '021-88888888',
'email' => 'support@example.com',
'hours' => 'شنبه تا پنج‌شنبه، 8 صبح تا 8 شب'
]
];
return $this->json([
'success' => true,
'data' => $helpInfo
]);
}
#[Route('/api/public/{businessId}/status', name: 'api_public_business_status', methods: ['GET'])]
public function getBusinessStatus(string $businessId, PluginService $pluginService, EntityManagerInterface $entityManager): JsonResponse
{
// Find business
$business = $entityManager->getRepository(Business::class)->find($businessId);
if (!$business) {
return $this->json([
'success' => false,
'message' => 'کسب و کار مورد نظر یافت نشد',
'error' => 'BUSINESS_NOT_FOUND'
], 404);
}
// Check if warranty plugin is active
if (!$pluginService->isActive('warranty', $business)) {
return $this->json([
'success' => false,
'message' => 'پلاگین گارانتی برای این کسب و کار فعال نیست',
'error' => 'PLUGIN_NOT_ACTIVE'
], 404);
}
return $this->json([
'success' => true,
'message' => 'کسب و کار و پلاگین گارانتی فعال است',
'data' => [
'businessName' => $business->getName(),
'pluginActive' => true
]
]);
}
#[Route('/api/public/{businessId}/warranty/qr-scan', name: 'api_public_warranty_qr_scan', methods: ['POST'])]
public function scanQrCode(string $businessId, Request $request, PluginService $pluginService, EntityManagerInterface $entityManager): JsonResponse
{
// Find business
$business = $entityManager->getRepository(Business::class)->find($businessId);
if (!$business) {
return $this->json([
'success' => false,
'message' => 'کسب و کار مورد نظر یافت نشد'
], 404);
}
// Check if warranty plugin is active
$pluginCheck = $this->checkWarrantyPluginActive($business, $pluginService);
if ($pluginCheck) {
return $pluginCheck;
}
// TODO: Implement QR code scanning logic
$params = json_decode($request->getContent(), true);
$qrData = $params['qrData'] ?? '';
if (empty($qrData)) {
return $this->json([
'success' => false,
'message' => 'داده QR خالی است'
], 400);
}
// Extract warranty code from QR data
// Assuming QR contains warranty code directly or in a URL
$warrantyCode = $this->extractWarrantyCodeFromQr($qrData);
if (!$warrantyCode) {
return $this->json([
'success' => false,
'message' => 'کد گارانتی در QR یافت نشد'
], 400);
}
return $this->json([
'success' => true,
'warrantyCode' => $warrantyCode
]);
}
private function extractWarrantyCodeFromQr(string $qrData): ?string
{
if (preg_match('/WR-\d{9}/', $qrData, $matches)) {
return $matches[0];
}
if (preg_match('/warranty.*code[=\/]([A-Za-z0-9-]+)/', $qrData, $matches)) {
return $matches[1];
}
return null;
}
}
function jalaliToGregorian($date) {
$p = explode('/', $date);
return implode('-', CalendarUtils::toGregorian($p[0], $p[1], $p[2]));
}

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,12 +389,15 @@ class SalaryController extends AbstractController
'year' => $acc['year'],
]);
if ($row) {
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $row->getDoc()->isApproved()) {
$transactions[] = $row;
}
}
}
}
}
}
$spreadsheet = new Spreadsheet();
$activeWorksheet = $spreadsheet->getActiveSheet();
$arrayEntity = [
@ -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,12 +484,15 @@ class SalaryController extends AbstractController
'year' => $acc['year'],
]);
if ($row) {
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $row->getDoc()->isApproved()) {
$transactions[] = $row;
}
}
}
}
}
}
$pid = $provider->createPrint(
$acc['bid'],
$this->getUser(),

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']
@ -68,6 +69,41 @@ class SellController extends AbstractController
]);
}
#[Route('/api/sell/approve/{code}', name: 'app_sell_approve', methods: ['POST'])]
public function approveSellDoc(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('sell');
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->setStatus('approved');
$entityManager->persist($doc);
$entityManager->flush();
return $this->json(['result' => 0]);
}
#[Route('/api/sell/payment/approve/{code}', name: 'app_sell_payment_approve', methods: ['POST'])]
public function approveSellPayment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('sell');
if (!$acc) throw $this->createAccessDeniedException();
$paymentDoc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
'money' => $acc['money'],
'type' => 'sell_receive'
]);
if (!$paymentDoc) throw $this->createNotFoundException('سند دریافت یافت نشد');
$paymentDoc->setStatus('approved');
$entityManager->persist($paymentDoc);
$entityManager->flush();
return $this->json(['result' => 0]);
}
#[Route('/api/sell/get/info/{code}', name: 'app_sell_get_info')]
public function app_sell_get_info(Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, string $code): JsonResponse
{
@ -127,224 +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'));
}
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);
// // حذف سندهای پرداخت قبلی
// $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'));
// ذخیره نوع تخفیف و درصد آن
$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);
// // 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);
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);
// // ذخیره نوع تخفیف و درصد آن
// $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);
$entityManager->persist($doc);
$entityManager->flush();
if (!$doc->getShortlink()) {
$doc->setShortlink($provider->RandomString(8));
}
// 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 (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 ($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));
// }
$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());
}
// 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());
// }
#[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
@ -424,10 +471,13 @@ class SellController extends AbstractController
$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')
@ -489,7 +539,9 @@ class SellController extends AbstractController
'plugin' => 'd.plugin',
'refData' => 'd.refData',
'shortlink' => 'd.shortlink',
'status' => 'd.status',
'isPreview' => 'd.isPreview',
'isApproved' => 'd.isApproved',
'approvedBy' => 'd.approvedBy',
'submitter' => 'u.fullName',
'label' => 'l.label', // از InvoiceLabel
];
@ -535,6 +587,13 @@ class SellController extends AbstractController
'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)
@ -621,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 {
@ -646,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 {
@ -778,6 +837,13 @@ class SellController extends AbstractController
$accountStatus['label'] = 'بدهکار';
$accountStatus['value'] = $bd - $bs;
}
$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) {
@ -829,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,
],
@ -990,6 +1057,9 @@ class SellController extends AbstractController
]);
}
$business = $entityManager->getRepository(Business::class)->find($acc['bid']);
$TwoStepApproval = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
try {
// بررسی وجود فاکتور برای ویرایش
if (!empty($params['id'])) {
@ -1031,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());
}
}
// تنظیم اطلاعات اصلی فاکتور
@ -1214,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);
@ -1270,6 +1359,7 @@ class SellController extends AbstractController
$receiveRef = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '3']);
$receiveRow->setRef($receiveRef);
$receiveRow->setPerson($person);
$entityManager->persist($receiveRow);
$entityManager->persist($paymentDoc);
@ -1342,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

@ -19,6 +19,8 @@ use App\Service\Log;
use App\Service\PluginService;
use App\Service\registryMGR;
use App\Service\SMS;
use App\Entity\PlugWarrantySerial;
use App\Entity\ArchiveFile;
use Doctrine\ORM\EntityManagerInterface;
use ReflectionException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -54,6 +56,90 @@ class StoreroomController extends AbstractController
);
}
#[Route('/api/storeroom/ticket/attachments/upload/{code}', name: 'app_storeroom_ticket_upload_attachment', methods: ['POST'])]
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('حواله یافت نشد');
$file = $request->files->get('file');
if (!$file) {
return $this->json(['result' => -1, 'message' => 'فایل ارسال نشده است'], 400);
}
$stored = $storage->store($file, (string) $acc['bid']->getId(), 'storeroom_attachments');
$archive = new ArchiveFile();
$archive->setBid($acc['bid']);
$archive->setSubmitter($acc['user']);
$archive->setDateSubmit(date('Y-m-d H:i:s'));
$archive->setFilename($stored['relativePath']);
$archive->setCat('storeroom_ticket');
$archive->setFileType($stored['mime'] ?: 'application/octet-stream');
$archive->setPublic(false);
$archive->setDes($request->request->get('des'));
$archive->setRelatedDocType('storeroom_ticket');
$archive->setRelatedDocCode($ticket->getCode());
$archive->setFileSize($stored['size'] !== null ? (string) $stored['size'] : null);
$entityManager->persist($archive);
$entityManager->flush();
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('حواله یافت نشد');
$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) {
return [
'id' => $a->getId(),
'filename' => $a->getFilename(),
'fileType' => $a->getFileType(),
'fileSize' => $a->getFileSize(),
'des' => $a->getDes(),
'dateSubmit' => $a->getDateSubmit(),
];
}, $items));
}
#[Route('/api/storeroom/ticket/attachments/download/{id}', name: 'app_storeroom_ticket_download_attachment', methods: ['GET'])]
public function downloadTicketAttachment(int $id, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): Response
{
$acc = $access->hasRole('store');
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());
if (!is_file($abs) || !is_readable($abs)) {
throw $this->createNotFoundException('فایل موجود نیست');
}
$response = new \Symfony\Component\HttpFoundation\BinaryFileResponse($abs);
$response->setContentDisposition(
\Symfony\Component\HttpFoundation\ResponseHeaderBag::DISPOSITION_ATTACHMENT,
basename($abs)
);
$response->headers->set('Content-Type', $a->getFileType() ?: 'application/octet-stream');
return $response;
}
#[Route('/api/storeroom/mod/{code}', name: 'app_storeroom_mod')]
public function app_storeroom_mod(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $code = 0): JsonResponse
{
@ -120,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)
@ -134,8 +220,12 @@ class StoreroomController extends AbstractController
foreach ($buys as $buy) {
$temp = $provider->Entity2Array($buy, 0);
$person = $this->getPerson($buy);
if ($person) {
$temp['person'] = Explore::ExplorePerson($person);
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
} else {
$temp['person'] = null;
}
$temp['commodities'] = $this->getCommodities($buy, $provider);
//check storeroom exist
$this->calcStoreRemaining($temp, $buy, $entityManager);
@ -153,10 +243,17 @@ class StoreroomController extends AbstractController
]);
$sellsForExport = [];
foreach ($sells as $sell) {
if ($sell->isPreview()) {
continue;
}
$temp = $provider->Entity2Array($sell, 0);
$person = $this->getPerson($sell);
if ($person) {
$temp['person'] = Explore::ExplorePerson($person);
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
} else {
$temp['person'] = null;
}
$temp['commodities'] = $this->getCommodities($sell, $provider);
//check storeroom exist
$this->calcStoreRemaining($temp, $sell, $entityManager);
@ -174,10 +271,17 @@ class StoreroomController extends AbstractController
]);
$rfsellsForExport = [];
foreach ($rfsells as $sell) {
if ($sell->isPreview()) {
continue;
}
$temp = $provider->Entity2Array($sell, 0);
$person = $this->getPerson($sell);
if ($person) {
$temp['person'] = Explore::ExplorePerson($person);
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
} else {
$temp['person'] = null;
}
$temp['commodities'] = $this->getCommodities($sell, $provider);
//check storeroom exist
$this->calcStoreRemaining($temp, $sell, $entityManager);
@ -195,10 +299,17 @@ class StoreroomController extends AbstractController
]);
$rfbuysForExport = [];
foreach ($rfbuys as $buy) {
if ($buy->isPreview()) {
continue;
}
$temp = $provider->Entity2Array($buy, 0);
$person = $this->getPerson($buy);
if ($person) {
$temp['person'] = Explore::ExplorePerson($person);
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
} else {
$temp['person'] = null;
}
$temp['commodities'] = $this->getCommodities($buy, $provider);
//check storeroom exist
$this->calcStoreRemaining($temp, $buy, $entityManager);
@ -281,8 +392,12 @@ class StoreroomController extends AbstractController
}
}
$res = $provider->Entity2Array($doc, 0);
if ($person) {
$res['person'] = $provider->Entity2Array($person, 0);
$res['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
} else {
$res['person'] = null;
}
$res['commodities'] = $provider->ArrayEntity2Array($commodities, 1, ['doc', 'bid', 'year']);
//calculate rows data
$this->calcStoreRemaining($res, $doc, $entityManager);
@ -356,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'],
@ -369,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']);
@ -384,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']);
@ -392,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']);
@ -403,6 +511,12 @@ class StoreroomController extends AbstractController
$ticket->setTransfer($params['ticket']['transfer']);
$ticket->setYear($acc['year']);
$ticket->setCode($provider->getAccountingCode($acc['bid'], 'storeroom'));
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$rand = '';
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);
$ticket->setReferral($params['ticket']['referral']);
@ -411,13 +525,29 @@ class StoreroomController extends AbstractController
$ticket->setType($params['ticket']['type']);
$ticket->setTypeString($params['ticket']['typeString']);
$ticket->setDes($params['ticket']['des']);
if (array_key_exists('importWorkflowCode', $params['ticket'])) {
$ticket->setImportWorkflowCode($params['ticket']['importWorkflowCode']);
}
$entityManager->persist($ticket);
//$entityManager->flush();
//going to save rows
$docRows = $entityManager->getRepository(HesabdariRow::class)->findBy([
'doc' => $doc
]);
$requireWarrantySerial = (
isset($params['ticket']['requireWarrantySerial'])
&& $params['ticket']['requireWarrantySerial'] === true
&& $pluginService->isActive('warranty', $acc['bid'])
);
if ($requireWarrantySerial) {
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);
}
}
}
foreach ($params['items'] as $item) {
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'bid' => $acc['bid'],
@ -428,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();
@ -441,12 +570,82 @@ class StoreroomController extends AbstractController
$ticketItem->setCommodity($row->getCommodity());
$ticketItem->setType($item['type']);
$entityManager->persist($ticketItem);
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
if ($requireWarrantySerial) {
if ((int) $item['ticketCount'] > 0) {
$entityManager->flush();
$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);
}
$serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
'business' => $acc['bid'],
'serialNumber' => $warrantyCode,
'commodity' => $row->getCommodity(),
]);
if (!$serial || $serial->getStatus() !== PlugWarrantySerial::STATUS_AVAILABLE) {
return $this->json(['result' => -4, 'message' => 'گارانتی نامعتبر یا آزاد نیست: ' . $warrantyCode], 400);
}
$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);
}
}
} 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();
//save logs
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool) $business->isRequireTwoStepApproval() : false;
if ($businessRequire) {
$ticket->setIsPreview(true);
$ticket->setIsApproved(false);
$ticket->setApprovedBy(null);
} else {
$ticket->setIsPreview(false);
$ticket->setIsApproved(true);
$ticket->setApprovedBy($this->getUser());
}
$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);
@ -489,7 +688,6 @@ class StoreroomController extends AbstractController
3
);
}
if ($smsres == 2) {
return $this->json([
'result' => 2
@ -497,12 +695,63 @@ class StoreroomController extends AbstractController
}
}
}
return $this->json([
'result' => 0
]);
}
#[Route('/api/storeroom/ticket/status/{code}', name: 'app_storeroom_ticket_status_update', methods: ['POST'])]
public function app_storeroom_ticket_status_update(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('store');
if (!$acc)
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'])) {
return $this->json(['result' => -1, 'message' => 'وضعیت نامعتبر'], 400);
}
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
if (!$ticket) {
throw $this->createNotFoundException('حواله یافت نشد.');
}
// $ticket->setStatus($status);
$entityManager->persist($ticket);
$entityManager->flush();
return $this->json(['result' => 0]);
}
#[Route('/api/storeroom/tickets', name: 'app_storeroom_tickets_by_status', methods: ['GET'])]
public function app_storeroom_tickets_by_status(Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('store');
if (!$acc)
throw $this->createAccessDeniedException();
$status = $request->query->get('status');
$criteria = [
'bid' => $acc['bid'],
'year' => $acc['year'],
];
if ($status) {
$criteria['status'] = $status;
}
$tickets = $entityManager->getRepository(StoreroomTicket::class)->findBy($criteria, ['date' => 'DESC']);
return $this->json(array_map(function (StoreroomTicket $t) {
return [
'code' => $t->getCode(),
'date' => $t->getDate(),
'type' => $t->getType(),
'typeString' => $t->getTypeString(),
'importWorkflowCode' => $t->getImportWorkflowCode(),
'person' => $t->getPerson() ? $t->getPerson()->getNikename() : null,
'storeroom' => $t->getStoreroom() ? $t->getStoreroom()->getName() : null,
];
}, $tickets));
}
#[Route('/api/storeroom/tickets/list/{type}', name: 'app_storeroom_tickets_list')]
public function app_storeroom_tickets_list(string $type, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
@ -516,15 +765,44 @@ class StoreroomController extends AbstractController
], [
'date' => 'DESC'
]);
return $this->json($provider->ArrayEntity2ArrayJustIncludes($tickets, [
$result = $provider->ArrayEntity2ArrayJustIncludes($tickets, [
'getDes',
'getCode',
'getDate',
'getPerson',
'getNikename',
'getDoc',
'getTypeString'
]));
'getTypeString',
'isPreview',
'isApproved',
'isCompleted'
], 2);
foreach ($result as $key => &$ticket) {
$ticketEntity = $tickets[$key];
if ($ticketEntity->getApprovedBy()) {
$approvedBy = $ticketEntity->getApprovedBy();
$ticket['approvedBy'] = [
'id' => $approvedBy->getId(),
'fullName' => $approvedBy->getFullName(),
'email' => $approvedBy->getEmail()
];
} 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);
}
#[Route('/api/storeroom/tickets/info/{code}', name: 'app_storeroom_ticket_view')]
@ -542,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'], 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
@ -659,6 +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 && $doc->isApproved() !== true && $doc->isPreview() == true) {
if ($doc->isPreview()) {
return $this->json(['result' => -10, 'message' => 'حواله هنوز تایید نشده است'], 403);
}
}
$pdfPid = 0;
$pdfPid = $provider->createPrint(
$acc['bid'],
@ -707,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');
while (true) {
if (!file_exists($stateFile)) {
echo "data: " . json_encode(['status' => 'idle', 'output' => '']) . "\n\n";
ob_flush();
flush();
break;
return new JsonResponse(['status' => 'idle', 'output' => '']);
}
$state = json_decode(file_get_contents($stateFile), true) ?? ['log' => ''];
$output = $state['log'] ?? '';
$isRunning = !isset($state['error']) &&
!in_array('post_update_test', $state['completedSteps'] ?? []);
!in_array('post_update_test', $state['completedSteps'] ?? []) &&
!str_contains($output, 'No update needed') &&
!str_contains($output, 'Software update completed successfully');
$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);
}
});
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

@ -117,6 +117,9 @@ class Business
#[ORM\Column(length: 255, nullable: true)]
private ?string $cashdeskCode = '1000';
#[ORM\Column(type: Types::BIGINT, nullable: true)]
private ?string $importWorkflowCode = null;
#[ORM\OneToMany(mappedBy: 'bid', targetEntity: Salary::class, orphanRemoval: true)]
private Collection $salaries;
@ -307,6 +310,55 @@ class Business
#[Ignore]
private Collection $plugWarrantySerials;
#[ORM\OneToMany(mappedBy: 'business', targetEntity: ImportWorkflow::class, orphanRemoval: true)]
private Collection $importWorkflows;
#[ORM\Column(nullable: true)]
private ?bool $requireTwoStepApproval = null;
// Two-step approval extended configuration
#[ORM\Column(nullable: true)]
private ?bool $approvalUseSameApprover = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverAll = null;
#[ORM\Column(length: 255, nullable: true)]
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()
{
$this->logs = new ArrayCollection();
@ -352,6 +404,7 @@ class Business
$this->plugHrmDocs = new ArrayCollection();
$this->aiConversations = new ArrayCollection();
$this->plugWarrantySerials = new ArrayCollection();
$this->importWorkflows = new ArrayCollection();
}
public function getId(): ?int
@ -879,6 +932,18 @@ class Business
return $this;
}
public function getImportWorkflowCode(): ?string
{
return $this->importWorkflowCode;
}
public function setImportWorkflowCode(?string $importWorkflowCode): self
{
$this->importWorkflowCode = $importWorkflowCode;
return $this;
}
/**
* @return Collection<int, Salary>
*/
@ -2155,4 +2220,171 @@ class Business
return $this;
}
public function getImportWorkflows(): Collection
{
return $this->importWorkflows;
}
public function addImportWorkflow(ImportWorkflow $importWorkflow): static
{
if (!$this->importWorkflows->contains($importWorkflow)) {
$this->importWorkflows->add($importWorkflow);
$importWorkflow->setBusiness($this);
}
return $this;
}
public function removeImportWorkflow(ImportWorkflow $importWorkflow): static
{
if ($this->importWorkflows->removeElement($importWorkflow)) {
if ($importWorkflow->getBusiness() === $this) {
$importWorkflow->setBusiness(null);
}
}
return $this;
}
public function isRequireTwoStepApproval(): ?bool
{
return $this->requireTwoStepApproval;
}
public function setRequireTwoStepApproval(?bool $requireTwoStepApproval): static
{
$this->requireTwoStepApproval = $requireTwoStepApproval;
return $this;
}
public function getApproverSellInvoice(): ?string
{
return $this->approverSellInvoice;
}
public function setApproverSellInvoice(?string $approverSellInvoice): static
{
$this->approverSellInvoice = $approverSellInvoice;
return $this;
}
public function getApproverBuyInvoice(): ?string
{
return $this->approverBuyInvoice;
}
public function setApproverBuyInvoice(?string $approverBuyInvoice): static
{
$this->approverBuyInvoice = $approverBuyInvoice;
return $this;
}
public function getApproverReturnBuy(): ?string
{
return $this->approverReturnBuy;
}
public function setApproverReturnBuy(?string $approverReturnBuy): static
{
$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

@ -726,4 +726,48 @@ class HesabdariDoc
return $this;
}
// Approval fields
#[ORM\Column(nullable: true, options: ['default' => false])]
private ?bool $isPreview = false;
#[ORM\Column(nullable: true, options: ['default' => true])]
private ?bool $isApproved = true;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true)]
private ?User $approvedBy = null;
public function isPreview(): ?bool
{
return $this->isPreview;
}
public function setIsPreview(?bool $isPreview): static
{
$this->isPreview = $isPreview;
return $this;
}
public function isApproved(): ?bool
{
return $this->isApproved;
}
public function setIsApproved(?bool $isApproved): static
{
$this->isApproved = $isApproved;
return $this;
}
public function getApprovedBy(): ?User
{
return $this->approvedBy;
}
public function setApprovedBy(?User $approvedBy): static
{
$this->approvedBy = $approvedBy;
return $this;
}
}

View file

@ -0,0 +1,423 @@
<?php
namespace App\Entity;
use App\Repository\ImportWorkflowRepository;
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: ImportWorkflowRepository::class)]
class ImportWorkflow
{
#[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 $title = null;
#[ORM\ManyToOne(inversedBy: 'importWorkflows')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Business $business = null;
#[ORM\ManyToOne(inversedBy: 'importWorkflows')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?User $submitter = null;
#[ORM\Column(length: 255)]
private ?string $dateSubmit = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $dateMod = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $status = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $supplierName = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $supplierCountry = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $supplierAddress = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $supplierPhone = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $supplierEmail = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $currency = null;
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $exchangeRate = null;
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowItem::class, orphanRemoval: true)]
private Collection $items;
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowPayment::class, orphanRemoval: true)]
private Collection $payments;
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowDocument::class, orphanRemoval: true)]
private Collection $documents;
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowStage::class, orphanRemoval: true)]
private Collection $stages;
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowShipping::class, orphanRemoval: true)]
private Collection $shipping;
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowCustoms::class, orphanRemoval: true)]
private Collection $customs;
public function __construct()
{
$this->items = new ArrayCollection();
$this->payments = new ArrayCollection();
$this->documents = new ArrayCollection();
$this->stages = new ArrayCollection();
$this->shipping = new ArrayCollection();
$this->customs = new ArrayCollection();
$this->dateSubmit = date('Y-m-d H:i:s');
$this->status = 'draft';
}
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 getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getBusiness(): ?Business
{
return $this->business;
}
public function setBusiness(?Business $business): static
{
$this->business = $business;
return $this;
}
public function getSubmitter(): ?User
{
return $this->submitter;
}
public function setSubmitter(?User $submitter): static
{
$this->submitter = $submitter;
return $this;
}
public function getDateSubmit(): ?string
{
return $this->dateSubmit;
}
public function setDateSubmit(string $dateSubmit): static
{
$this->dateSubmit = $dateSubmit;
return $this;
}
public function getDateMod(): ?string
{
return $this->dateMod;
}
public function setDateMod(?string $dateMod): static
{
$this->dateMod = $dateMod;
return $this;
}
public function getStatus(): ?string
{
return $this->status;
}
public function setStatus(?string $status): static
{
$this->status = $status;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getSupplierName(): ?string
{
return $this->supplierName;
}
public function setSupplierName(?string $supplierName): static
{
$this->supplierName = $supplierName;
return $this;
}
public function getSupplierCountry(): ?string
{
return $this->supplierCountry;
}
public function setSupplierCountry(?string $supplierCountry): static
{
$this->supplierCountry = $supplierCountry;
return $this;
}
public function getSupplierAddress(): ?string
{
return $this->supplierAddress;
}
public function setSupplierAddress(?string $supplierAddress): static
{
$this->supplierAddress = $supplierAddress;
return $this;
}
public function getSupplierPhone(): ?string
{
return $this->supplierPhone;
}
public function setSupplierPhone(?string $supplierPhone): static
{
$this->supplierPhone = $supplierPhone;
return $this;
}
public function getSupplierEmail(): ?string
{
return $this->supplierEmail;
}
public function setSupplierEmail(?string $supplierEmail): static
{
$this->supplierEmail = $supplierEmail;
return $this;
}
public function getCurrency(): ?string
{
return $this->currency;
}
public function setCurrency(?string $currency): static
{
$this->currency = $currency;
return $this;
}
public function getExchangeRate(): ?string
{
return $this->exchangeRate;
}
public function setExchangeRate(?string $exchangeRate): static
{
$this->exchangeRate = $exchangeRate;
return $this;
}
public function getItems(): Collection
{
return $this->items;
}
public function addItem(ImportWorkflowItem $item): static
{
if (!$this->items->contains($item)) {
$this->items->add($item);
$item->setImportWorkflow($this);
}
return $this;
}
public function removeItem(ImportWorkflowItem $item): static
{
if ($this->items->removeElement($item)) {
if ($item->getImportWorkflow() === $this) {
$item->setImportWorkflow(null);
}
}
return $this;
}
public function getPayments(): Collection
{
return $this->payments;
}
public function addPayment(ImportWorkflowPayment $payment): static
{
if (!$this->payments->contains($payment)) {
$this->payments->add($payment);
$payment->setImportWorkflow($this);
}
return $this;
}
public function removePayment(ImportWorkflowPayment $payment): static
{
if ($this->payments->removeElement($payment)) {
if ($payment->getImportWorkflow() === $this) {
$payment->setImportWorkflow(null);
}
}
return $this;
}
public function getDocuments(): Collection
{
return $this->documents;
}
public function addDocument(ImportWorkflowDocument $document): static
{
if (!$this->documents->contains($document)) {
$this->documents->add($document);
$document->setImportWorkflow($this);
}
return $this;
}
public function removeDocument(ImportWorkflowDocument $document): static
{
if ($this->documents->removeElement($document)) {
if ($document->getImportWorkflow() === $this) {
$document->setImportWorkflow(null);
}
}
return $this;
}
public function getStages(): Collection
{
return $this->stages;
}
public function addStage(ImportWorkflowStage $stage): static
{
if (!$this->stages->contains($stage)) {
$this->stages->add($stage);
$stage->setImportWorkflow($this);
}
return $this;
}
public function removeStage(ImportWorkflowStage $stage): static
{
if ($this->stages->removeElement($stage)) {
if ($stage->getImportWorkflow() === $this) {
$stage->setImportWorkflow(null);
}
}
return $this;
}
public function getShipping(): Collection
{
return $this->shipping;
}
public function addShipping(ImportWorkflowShipping $shipping): static
{
if (!$this->shipping->contains($shipping)) {
$this->shipping->add($shipping);
$shipping->setImportWorkflow($this);
}
return $this;
}
public function removeShipping(ImportWorkflowShipping $shipping): static
{
if ($this->shipping->removeElement($shipping)) {
if ($shipping->getImportWorkflow() === $this) {
$shipping->setImportWorkflow(null);
}
}
return $this;
}
public function getCustoms(): Collection
{
return $this->customs;
}
public function addCustom(ImportWorkflowCustoms $custom): static
{
if (!$this->customs->contains($custom)) {
$this->customs->add($custom);
$custom->setImportWorkflow($this);
}
return $this;
}
public function removeCustom(ImportWorkflowCustoms $custom): static
{
if ($this->customs->removeElement($custom)) {
if ($custom->getImportWorkflow() === $this) {
$custom->setImportWorkflow(null);
}
}
return $this;
}
public function getComputedTotalAmount(): ?string
{
$items = $this->getItems();
$total = 0;
foreach ($items as $item) {
$total += $item->getTotalPrice();
}
return $total;
}
}

View file

@ -0,0 +1,254 @@
<?php
namespace App\Entity;
use App\Repository\ImportWorkflowCustomsRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: ImportWorkflowCustomsRepository::class)]
class ImportWorkflowCustoms
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'customs')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?ImportWorkflow $importWorkflow = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $declarationNumber = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $customsCode = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $clearanceDate = null;
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $customsDuty = null;
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $valueAddedTax = null;
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $otherCharges = null;
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $totalCustomsCharges = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $customsBroker = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $customsBrokerPhone = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $customsBrokerEmail = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $warehouseNumber = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $warehouseLocation = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 255)]
private ?string $dateSubmit = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $status = null;
public function __construct()
{
$this->dateSubmit = date('Y-m-d H:i:s');
$this->status = 'pending';
}
public function getId(): ?int
{
return $this->id;
}
public function getImportWorkflow(): ?ImportWorkflow
{
return $this->importWorkflow;
}
public function setImportWorkflow(?ImportWorkflow $importWorkflow): static
{
$this->importWorkflow = $importWorkflow;
return $this;
}
public function getDeclarationNumber(): ?string
{
return $this->declarationNumber;
}
public function setDeclarationNumber(?string $declarationNumber): static
{
$this->declarationNumber = $declarationNumber;
return $this;
}
public function getCustomsCode(): ?string
{
return $this->customsCode;
}
public function setCustomsCode(?string $customsCode): static
{
$this->customsCode = $customsCode;
return $this;
}
public function getClearanceDate(): ?string
{
return $this->clearanceDate;
}
public function setClearanceDate(?string $clearanceDate): static
{
$this->clearanceDate = $clearanceDate;
return $this;
}
public function getCustomsDuty(): ?string
{
return $this->customsDuty;
}
public function setCustomsDuty(?string $customsDuty): static
{
$this->customsDuty = $customsDuty;
return $this;
}
public function getValueAddedTax(): ?string
{
return $this->valueAddedTax;
}
public function setValueAddedTax(?string $valueAddedTax): static
{
$this->valueAddedTax = $valueAddedTax;
return $this;
}
public function getOtherCharges(): ?string
{
return $this->otherCharges;
}
public function setOtherCharges(?string $otherCharges): static
{
$this->otherCharges = $otherCharges;
return $this;
}
public function getTotalCustomsCharges(): ?string
{
return $this->totalCustomsCharges;
}
public function setTotalCustomsCharges(?string $totalCustomsCharges): static
{
$this->totalCustomsCharges = $totalCustomsCharges;
return $this;
}
public function getCustomsBroker(): ?string
{
return $this->customsBroker;
}
public function setCustomsBroker(?string $customsBroker): static
{
$this->customsBroker = $customsBroker;
return $this;
}
public function getCustomsBrokerPhone(): ?string
{
return $this->customsBrokerPhone;
}
public function setCustomsBrokerPhone(?string $customsBrokerPhone): static
{
$this->customsBrokerPhone = $customsBrokerPhone;
return $this;
}
public function getCustomsBrokerEmail(): ?string
{
return $this->customsBrokerEmail;
}
public function setCustomsBrokerEmail(?string $customsBrokerEmail): static
{
$this->customsBrokerEmail = $customsBrokerEmail;
return $this;
}
public function getWarehouseNumber(): ?string
{
return $this->warehouseNumber;
}
public function setWarehouseNumber(?string $warehouseNumber): static
{
$this->warehouseNumber = $warehouseNumber;
return $this;
}
public function getWarehouseLocation(): ?string
{
return $this->warehouseLocation;
}
public function setWarehouseLocation(?string $warehouseLocation): static
{
$this->warehouseLocation = $warehouseLocation;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getDateSubmit(): ?string
{
return $this->dateSubmit;
}
public function setDateSubmit(string $dateSubmit): static
{
$this->dateSubmit = $dateSubmit;
return $this;
}
public function getStatus(): ?string
{
return $this->status;
}
public function setStatus(?string $status): static
{
$this->status = $status;
return $this;
}
}

View file

@ -0,0 +1,213 @@
<?php
namespace App\Entity;
use App\Repository\ImportWorkflowDocumentRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: ImportWorkflowDocumentRepository::class)]
class ImportWorkflowDocument
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'documents')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?ImportWorkflow $importWorkflow = null;
#[ORM\Column(length: 255)]
private ?string $type = null;
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $filePath = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $fileName = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $fileSize = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $fileType = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 255)]
private ?string $dateSubmit = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $documentNumber = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $issueDate = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $expiryDate = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $status = null;
public function __construct()
{
$this->dateSubmit = date('Y-m-d H:i:s');
$this->status = 'active';
}
public function getId(): ?int
{
return $this->id;
}
public function getImportWorkflow(): ?ImportWorkflow
{
return $this->importWorkflow;
}
public function setImportWorkflow(?ImportWorkflow $importWorkflow): static
{
$this->importWorkflow = $importWorkflow;
return $this;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(string $type): static
{
$this->type = $type;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getFilePath(): ?string
{
return $this->filePath;
}
public function setFilePath(?string $filePath): static
{
$this->filePath = $filePath;
return $this;
}
public function getFileName(): ?string
{
return $this->fileName;
}
public function setFileName(?string $fileName): static
{
$this->fileName = $fileName;
return $this;
}
public function getFileSize(): ?string
{
return $this->fileSize;
}
public function setFileSize(?string $fileSize): static
{
$this->fileSize = $fileSize;
return $this;
}
public function getFileType(): ?string
{
return $this->fileType;
}
public function setFileType(?string $fileType): static
{
$this->fileType = $fileType;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getDateSubmit(): ?string
{
return $this->dateSubmit;
}
public function setDateSubmit(string $dateSubmit): static
{
$this->dateSubmit = $dateSubmit;
return $this;
}
public function getDocumentNumber(): ?string
{
return $this->documentNumber;
}
public function setDocumentNumber(?string $documentNumber): static
{
$this->documentNumber = $documentNumber;
return $this;
}
public function getIssueDate(): ?string
{
return $this->issueDate;
}
public function setIssueDate(?string $issueDate): static
{
$this->issueDate = $issueDate;
return $this;
}
public function getExpiryDate(): ?string
{
return $this->expiryDate;
}
public function setExpiryDate(?string $expiryDate): static
{
$this->expiryDate = $expiryDate;
return $this;
}
public function getStatus(): ?string
{
return $this->status;
}
public function setStatus(?string $status): static
{
$this->status = $status;
return $this;
}
}

View file

@ -0,0 +1,270 @@
<?php
namespace App\Entity;
use App\Repository\ImportWorkflowItemRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
use App\Entity\Commodity;
#[ORM\Entity(repositoryClass: ImportWorkflowItemRepository::class)]
class ImportWorkflowItem
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'items')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?ImportWorkflow $importWorkflow = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true)]
private ?Commodity $commodity = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $productCode = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $brand = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $model = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $originCountry = null;
#[ORM\Column(length: 255)]
private ?string $quantity = null;
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $unitPrice = null;
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $unitPriceIRR = null;
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $totalPrice = null;
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $totalPriceIRR = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $weight = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $volume = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $specifications = null;
#[ORM\Column(length: 255)]
private ?string $dateSubmit = null;
public function __construct()
{
$this->dateSubmit = date('Y-m-d H:i:s');
}
public function getId(): ?int
{
return $this->id;
}
public function getImportWorkflow(): ?ImportWorkflow
{
return $this->importWorkflow;
}
public function setImportWorkflow(?ImportWorkflow $importWorkflow): static
{
$this->importWorkflow = $importWorkflow;
return $this;
}
public function getCommodity(): ?Commodity
{
return $this->commodity;
}
public function setCommodity(?Commodity $commodity): static
{
$this->commodity = $commodity;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getProductCode(): ?string
{
return $this->productCode;
}
public function setProductCode(?string $productCode): static
{
$this->productCode = $productCode;
return $this;
}
public function getBrand(): ?string
{
return $this->brand;
}
public function setBrand(?string $brand): static
{
$this->brand = $brand;
return $this;
}
public function getModel(): ?string
{
return $this->model;
}
public function setModel(?string $model): static
{
$this->model = $model;
return $this;
}
public function getOriginCountry(): ?string
{
return $this->originCountry;
}
public function setOriginCountry(?string $originCountry): static
{
$this->originCountry = $originCountry;
return $this;
}
public function getQuantity(): ?string
{
return $this->quantity;
}
public function setQuantity(string $quantity): static
{
$this->quantity = $quantity;
return $this;
}
public function getUnitPrice(): ?string
{
return $this->unitPrice;
}
public function setUnitPrice(?string $unitPrice): static
{
$this->unitPrice = $unitPrice;
return $this;
}
public function getUnitPriceIRR(): ?string
{
return $this->unitPriceIRR;
}
public function setUnitPriceIRR(?string $unitPriceIRR): static
{
$this->unitPriceIRR = $unitPriceIRR;
return $this;
}
public function getTotalPrice(): ?string
{
return $this->totalPrice;
}
public function setTotalPrice(?string $totalPrice): static
{
$this->totalPrice = $totalPrice;
return $this;
}
public function getTotalPriceIRR(): ?string
{
return $this->totalPriceIRR;
}
public function setTotalPriceIRR(?string $totalPriceIRR): static
{
$this->totalPriceIRR = $totalPriceIRR;
return $this;
}
public function getWeight(): ?string
{
return $this->weight;
}
public function setWeight(?string $weight): static
{
$this->weight = $weight;
return $this;
}
public function getVolume(): ?string
{
return $this->volume;
}
public function setVolume(?string $volume): static
{
$this->volume = $volume;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getSpecifications(): ?string
{
return $this->specifications;
}
public function setSpecifications(?string $specifications): static
{
$this->specifications = $specifications;
return $this;
}
public function getDateSubmit(): ?string
{
return $this->dateSubmit;
}
public function setDateSubmit(string $dateSubmit): static
{
$this->dateSubmit = $dateSubmit;
return $this;
}
}

View file

@ -0,0 +1,227 @@
<?php
namespace App\Entity;
use App\Repository\ImportWorkflowPaymentRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: ImportWorkflowPaymentRepository::class)]
class ImportWorkflowPayment
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'payments')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?ImportWorkflow $importWorkflow = null;
#[ORM\Column(length: 255)]
private ?string $type = null;
#[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(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $amountIRR = null;
#[ORM\Column(length: 255)]
private ?string $paymentDate = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $referenceNumber = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $bankName = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $accountNumber = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $recipientName = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $status = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $receiptNumber = null;
#[ORM\Column(length: 255)]
private ?string $dateSubmit = null;
public function __construct()
{
$this->dateSubmit = date('Y-m-d H:i:s');
$this->status = 'pending';
}
public function getId(): ?int
{
return $this->id;
}
public function getImportWorkflow(): ?ImportWorkflow
{
return $this->importWorkflow;
}
public function setImportWorkflow(?ImportWorkflow $importWorkflow): static
{
$this->importWorkflow = $importWorkflow;
return $this;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(string $type): static
{
$this->type = $type;
return $this;
}
public function getAmount(): ?string
{
return $this->amount;
}
public function setAmount(string $amount): static
{
$this->amount = $amount;
return $this;
}
public function getCurrency(): ?string
{
return $this->currency;
}
public function setCurrency(?string $currency): static
{
$this->currency = $currency;
return $this;
}
public function getAmountIRR(): ?string
{
return $this->amountIRR;
}
public function setAmountIRR(?string $amountIRR): static
{
$this->amountIRR = $amountIRR;
return $this;
}
public function getPaymentDate(): ?string
{
return $this->paymentDate;
}
public function setPaymentDate(string $paymentDate): static
{
$this->paymentDate = $paymentDate;
return $this;
}
public function getReferenceNumber(): ?string
{
return $this->referenceNumber;
}
public function setReferenceNumber(?string $referenceNumber): static
{
$this->referenceNumber = $referenceNumber;
return $this;
}
public function getBankName(): ?string
{
return $this->bankName;
}
public function setBankName(?string $bankName): static
{
$this->bankName = $bankName;
return $this;
}
public function getAccountNumber(): ?string
{
return $this->accountNumber;
}
public function setAccountNumber(?string $accountNumber): static
{
$this->accountNumber = $accountNumber;
return $this;
}
public function getRecipientName(): ?string
{
return $this->recipientName;
}
public function setRecipientName(?string $recipientName): static
{
$this->recipientName = $recipientName;
return $this;
}
public function getStatus(): ?string
{
return $this->status;
}
public function setStatus(?string $status): static
{
$this->status = $status;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getReceiptNumber(): ?string
{
return $this->receiptNumber;
}
public function setReceiptNumber(?string $receiptNumber): static
{
$this->receiptNumber = $receiptNumber;
return $this;
}
public function getDateSubmit(): ?string
{
return $this->dateSubmit;
}
public function setDateSubmit(string $dateSubmit): static
{
$this->dateSubmit = $dateSubmit;
return $this;
}
}

View file

@ -0,0 +1,269 @@
<?php
namespace App\Entity;
use App\Repository\ImportWorkflowShippingRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: ImportWorkflowShippingRepository::class)]
class ImportWorkflowShipping
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'shipping')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?ImportWorkflow $importWorkflow = null;
#[ORM\Column(length: 255)]
private ?string $type = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $containerNumber = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $billOfLading = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $shippingDate = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $arrivalDate = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $unloadingDate = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $shippingCompany = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $shippingCompanyPhone = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $shippingCompanyEmail = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $originPort = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $destinationPort = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $vesselName = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $voyageNumber = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 255)]
private ?string $dateSubmit = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $status = null;
public function __construct()
{
$this->dateSubmit = date('Y-m-d H:i:s');
$this->status = 'pending';
}
public function getId(): ?int
{
return $this->id;
}
public function getImportWorkflow(): ?ImportWorkflow
{
return $this->importWorkflow;
}
public function setImportWorkflow(?ImportWorkflow $importWorkflow): static
{
$this->importWorkflow = $importWorkflow;
return $this;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(string $type): static
{
$this->type = $type;
return $this;
}
public function getContainerNumber(): ?string
{
return $this->containerNumber;
}
public function setContainerNumber(?string $containerNumber): static
{
$this->containerNumber = $containerNumber;
return $this;
}
public function getBillOfLading(): ?string
{
return $this->billOfLading;
}
public function setBillOfLading(?string $billOfLading): static
{
$this->billOfLading = $billOfLading;
return $this;
}
public function getShippingDate(): ?string
{
return $this->shippingDate;
}
public function setShippingDate(?string $shippingDate): static
{
$this->shippingDate = $shippingDate;
return $this;
}
public function getArrivalDate(): ?string
{
return $this->arrivalDate;
}
public function setArrivalDate(?string $arrivalDate): static
{
$this->arrivalDate = $arrivalDate;
return $this;
}
public function getUnloadingDate(): ?string
{
return $this->unloadingDate;
}
public function setUnloadingDate(?string $unloadingDate): static
{
$this->unloadingDate = $unloadingDate;
return $this;
}
public function getShippingCompany(): ?string
{
return $this->shippingCompany;
}
public function setShippingCompany(?string $shippingCompany): static
{
$this->shippingCompany = $shippingCompany;
return $this;
}
public function getShippingCompanyPhone(): ?string
{
return $this->shippingCompanyPhone;
}
public function setShippingCompanyPhone(?string $shippingCompanyPhone): static
{
$this->shippingCompanyPhone = $shippingCompanyPhone;
return $this;
}
public function getShippingCompanyEmail(): ?string
{
return $this->shippingCompanyEmail;
}
public function setShippingCompanyEmail(?string $shippingCompanyEmail): static
{
$this->shippingCompanyEmail = $shippingCompanyEmail;
return $this;
}
public function getOriginPort(): ?string
{
return $this->originPort;
}
public function setOriginPort(?string $originPort): static
{
$this->originPort = $originPort;
return $this;
}
public function getDestinationPort(): ?string
{
return $this->destinationPort;
}
public function setDestinationPort(?string $destinationPort): static
{
$this->destinationPort = $destinationPort;
return $this;
}
public function getVesselName(): ?string
{
return $this->vesselName;
}
public function setVesselName(?string $vesselName): static
{
$this->vesselName = $vesselName;
return $this;
}
public function getVoyageNumber(): ?string
{
return $this->voyageNumber;
}
public function setVoyageNumber(?string $voyageNumber): static
{
$this->voyageNumber = $voyageNumber;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getDateSubmit(): ?string
{
return $this->dateSubmit;
}
public function setDateSubmit(string $dateSubmit): static
{
$this->dateSubmit = $dateSubmit;
return $this;
}
public function getStatus(): ?string
{
return $this->status;
}
public function setStatus(?string $status): static
{
$this->status = $status;
return $this;
}
}

View file

@ -0,0 +1,157 @@
<?php
namespace App\Entity;
use App\Repository\ImportWorkflowStageRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: ImportWorkflowStageRepository::class)]
class ImportWorkflowStage
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'stages')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?ImportWorkflow $importWorkflow = null;
#[ORM\Column(length: 255)]
private ?string $stage = null;
#[ORM\Column(length: 255)]
private ?string $status = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $startDate = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $endDate = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $assignedTo = null;
#[ORM\Column(length: 255)]
private ?string $dateSubmit = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $notes = null;
public function __construct()
{
$this->dateSubmit = date('Y-m-d H:i:s');
$this->status = 'pending';
}
public function getId(): ?int
{
return $this->id;
}
public function getImportWorkflow(): ?ImportWorkflow
{
return $this->importWorkflow;
}
public function setImportWorkflow(?ImportWorkflow $importWorkflow): static
{
$this->importWorkflow = $importWorkflow;
return $this;
}
public function getStage(): ?string
{
return $this->stage;
}
public function setStage(string $stage): static
{
$this->stage = $stage;
return $this;
}
public function getStatus(): ?string
{
return $this->status;
}
public function setStatus(string $status): static
{
$this->status = $status;
return $this;
}
public function getStartDate(): ?string
{
return $this->startDate;
}
public function setStartDate(?string $startDate): static
{
$this->startDate = $startDate;
return $this;
}
public function getEndDate(): ?string
{
return $this->endDate;
}
public function setEndDate(?string $endDate): static
{
$this->endDate = $endDate;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getAssignedTo(): ?string
{
return $this->assignedTo;
}
public function setAssignedTo(?string $assignedTo): static
{
$this->assignedTo = $assignedTo;
return $this;
}
public function getDateSubmit(): ?string
{
return $this->dateSubmit;
}
public function setDateSubmit(string $dateSubmit): static
{
$this->dateSubmit = $dateSubmit;
return $this;
}
public function getNotes(): ?string
{
return $this->notes;
}
public function setNotes(?string $notes): static
{
$this->notes = $notes;
return $this;
}
}

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

@ -141,6 +141,15 @@ class Permission
#[ORM\Column(nullable: true)]
private ?bool $ai = null;
#[ORM\Column(nullable: true)]
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;
@ -649,4 +658,40 @@ class Permission
return $this;
}
public function isStorehelper(): ?bool
{
return $this->storehelper;
}
public function setStorehelper(?bool $storehelper): static
{
$this->storehelper = $storehelper;
return $this;
}
public function isImportWorkflow(): ?bool
{
return $this->importWorkflow;
}
public function setImportWorkflow(?bool $importWorkflow): static
{
$this->importWorkflow = $importWorkflow;
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,10 @@ 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()
{
$this->hesabdariRows = new ArrayCollection();
@ -913,4 +917,17 @@ class Person
$this->tags = $tags;
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;
}
}

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