Compare commits

...

133 commits

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
Hesabix cd6821969f remove sign of incurrect developer 2025-08-10 23:41:38 +00:00
Hesabix ba9fe02ce7 addcustom invoice template plugin 2025-08-09 16:24:45 +00:00
Hesabix e9f2a14a27 bug fix final in remote docs 2025-08-09 08:32:56 +00:00
Hesabix 8704f6b4f4 bug fix in delete logs 2025-08-09 08:23:03 +00:00
Hesabix b477bd47fa Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-09 08:21:45 +00:00
Hesabix 5a12f2cbc5 bug fix in delete docs and start working on custome invoice design 2025-08-09 08:21:31 +00:00
Gloomy 97052b2fd8 update for Moadian plugin 2025-08-08 11:46:55 +00:00
Gloomy 676f65da86 update for Moadian plugin 2025-08-08 11:43:08 +00:00
Gloomy 4d8f6f46f7 update HookService 2025-08-07 19:17:17 +00:00
Hesabix 8ead39e274 fix dubkucate codes in bank account 2025-08-07 17:12:37 +00:00
Hesabix 55cf1e5d6d add change business to dashboard 2025-08-06 22:26:26 +00:00
Hesabix fd735320c3 bug fix in hook autoload service 2025-08-06 17:36:24 +00:00
Hesabix 3336379e23 Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-06 17:01:57 +00:00
Hesabix 1d47722cf9 bug fix in pre invoice 2025-08-06 17:01:54 +00:00
Gloomy aae6a74b0c Warranty module added (draft only) 2025-08-06 11:46:28 +00:00
Gloomy 21fb6b7c09 update for Moadian plugin 2025-08-06 10:39:03 +00:00
Hesabix 40fbedb6d1 bug fix in persons paginations 2025-08-06 08:06:31 +00:00
Gloomy 36c2841011 update for Hooks 2025-08-05 22:09:36 +00:00
Hesabix 82d39dbb42 add debug to system managment 2025-08-04 22:44:32 +00:00
Hesabix 11caf42da8 bug fix in cheque and add support person transactions sort by 2025-08-04 15:02:55 +00:00
Hesabix 63b6654cc8 bug fix in commodity custome code 2025-08-04 13:51:04 +00:00
Hesabix 532ca041f6 add chat system 2025-08-04 13:31:07 +00:00
Hesabix 6a4254050d Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-03 12:38:19 +00:00
Hesabix a7636fbc42 add chat system 2025-08-03 12:38:15 +00:00
Gloomy 82f872eb10 update for Moadian plugin 2025-08-03 11:50:58 +00:00
Gloomy 140da029a1 update for Moadian Plugin 2025-08-03 11:15:03 +00:00
Gloomy 300d802ee8 update for Moadian Plugin 2025-08-03 06:12:05 +00:00
Gloomy 8e8ea18ec9 update for Moadian Plugin 2025-08-02 19:31:29 +00:00
Gloomy d3e936c59f update for Moadian plugin 2025-07-28 14:00:21 +00:00
Gloomy 163ec1ea2e update for hooks 2025-07-28 13:56:52 +00:00
Gloomy be782e14bd update for Hooks 2025-07-27 14:27:17 +00:00
Hesabix 195e6c0693 last bug fix in tonight 2025-07-26 12:46:50 -07:00
Hesabix 88bc35d85d more more bug fix in ticket tools 2025-07-26 12:34:04 -07:00
Hesabix 82623c0df8 more bug fix 2025-07-26 12:28:33 -07:00
Hesabix ad638e960f bugfix 2025-07-26 12:19:44 -07:00
Hesabix 1b4b9f85f2 bug fix 2025-07-26 11:58:03 -07:00
Hesabix 29cc20207f add ticket tool for AGI 2025-07-26 11:49:32 -07:00
Hesabix aaeb3cf31e progress and some bug fix in commodity,ai 2025-07-25 17:12:20 +00:00
Hesabix 91cf5d4eb6 add type selection to accounting table 2025-07-25 11:34:36 +00:00
Hesabix cc1515345b progress in salary and add export pdf/excell to that 2025-07-25 09:54:43 +00:00
Hesabix 8bc857c2f8 increase decimals to 3 digits 2025-07-24 22:13:15 +00:00
Hesabix 3c2bef6685 bug fix in accounting search 2025-07-24 21:58:50 +00:00
Hesabix bf6ca0f8b6 bug fix in presell invoice in find sell price 2025-07-24 21:45:19 +00:00
Hesabix bad8dc0f73 more progress in agi and support external tools 2025-07-24 19:19:53 +00:00
Hesabix 474f1274c0 Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-07-24 12:55:48 +00:00
Hesabix a095dd530f progress in AGI 2025-07-24 12:55:44 +00:00
Gloomy 6720cc1774 Upate for Moadian plugin 2025-07-24 11:38:36 +00:00
Hesabix 3047c62f5d bug fix in save settings in zero day 2025-07-24 01:17:40 +00:00
Hesabix 4b286c481e bug fix for doctrine new version config change 2025-07-24 00:11:03 +00:00
Hesabix 92e6ecaee1 Update README.md 2025-07-24 03:14:18 +03:30
Gloomy 21aaad7ef1 Upate for Moadian plugin 2025-07-23 18:34:27 +00:00
324 changed files with 379815 additions and 5403 deletions

View file

@ -1,127 +0,0 @@
# هوش مصنوعی حسابیکس - یکپارچه‌سازی با اطلاعات اشخاص
## خلاصه
این پروژه قابلیت‌های جدیدی به سیستم هوش مصنوعی حسابیکس اضافه کرده است که به کاربران امکان دسترسی پویا به اطلاعات اشخاص را می‌دهد. هوش مصنوعی حالا می‌تواند به سوالات مربوط به اشخاص، موجودی‌ها و تراکنش‌های مالی پاسخ دهد.
## ویژگی‌های جدید
### 1. دسترسی به اطلاعات اشخاص
- نمایش اطلاعات کامل اشخاص شامل نام، کد، آدرس، تلفن و غیره
- محاسبه و نمایش موجودی مالی اشخاص
- نمایش تراکنش‌های اخیر هر شخص
- نمایش کارت‌های بانکی و اطلاعات مالی
### 2. جستجوی هوشمند
- جستجو بر اساس نام، کد یا شماره تلفن
- پیشنهادات جستجو
- فیلتر بر اساس نوع اشخاص (مشتری، تامین‌کننده، کارمند)
### 3. امنیت و حریم خصوصی
- هر کاربر فقط به اطلاعات اشخاص کسب و کار خود دسترسی دارد
- بررسی دسترسی‌ها قبل از نمایش اطلاعات
- محافظت از اطلاعات حساس
## ساختار فایل‌ها
### Backend (PHP/Symfony)
#### سرویس‌های جدید:
- `PersonDataService.php`: مدیریت داده‌های اشخاص
- `AIService.php`: به‌روزرسانی شده برای پشتیبانی از اطلاعات اشخاص
#### کنترلرهای جدید:
- `wizardController.php`: اضافه شدن endpoint های جدید برای اشخاص
#### API Endpoints جدید:
- `POST /api/wizard/persons/search`: جستجوی اشخاص
- `GET /api/wizard/persons/{personId}`: دریافت اطلاعات شخص
- `GET /api/wizard/persons/{personId}/transactions`: دریافت تراکنش‌های شخص
### Frontend (Vue.js)
#### کامپوننت‌های جدید:
- `PersonInfo.vue`: نمایش اطلاعات کامل شخص
- `home.vue`: به‌روزرسانی شده برای پشتیبانی از قابلیت‌های جدید
## نحوه استفاده
### 1. سوالات مربوط به اشخاص
کاربران می‌توانند سوالاتی مانند موارد زیر بپرسند:
- "اطلاعات شخص احمد محمدی"
- "موجودی مشتری علی رضایی"
- "تراکنش‌های تامین‌کننده شرکت ABC"
- "لیست کارمندان"
### 2. جستجوی مستقیم
- استفاده از پیشنهادات موجود در رابط کاربری
- تایپ نام یا کد شخص در چت
### 3. نمایش اطلاعات
- اطلاعات شخص در دیالوگ جداگانه نمایش داده می‌شود
- شامل موجودی مالی، تراکنش‌ها و اطلاعات تماس
- امکان مشاهده جزئیات کامل
## امنیت
### بررسی دسترسی‌ها:
- هر درخواست ابتدا بررسی می‌شود که کاربر دسترسی لازم را داشته باشد
- اطلاعات فقط برای کسب و کار مربوطه نمایش داده می‌شود
- API endpoints محافظت شده با سیستم احراز هویت
### محافظت از داده‌ها:
- شماره کارت‌های بانکی ماسک می‌شوند
- اطلاعات حساس فیلتر می‌شوند
- لاگ تمام درخواست‌ها ثبت می‌شود
## تنظیمات
### پرامپ هوش مصنوعی:
سیستم به طور خودکار اطلاعات اشخاص را به پرامپ اضافه می‌کند تا هوش مصنوعی بتواند به سوالات مربوطه پاسخ دهد.
### محدودیت‌ها:
- حداکثر 20 نتیجه در جستجو
- حداکثر 10 تراکنش در نمایش
- محدودیت دسترسی بر اساس کسب و کار
## نمونه استفاده
```javascript
// جستجوی شخص
const persons = await this.searchPersons('احمد محمدی');
// دریافت اطلاعات شخص
const personDetails = await this.getPersonDetails(personId);
// دریافت تراکنش‌ها
const transactions = await this.getPersonTransactions(personId, 10);
```
## آینده‌نگری
### قابلیت‌های پیشنهادی:
1. گزارش‌گیری پیشرفته از اشخاص
2. تحلیل روند تراکنش‌ها
3. پیش‌بینی موجودی بر اساس الگوهای گذشته
4. یکپارچه‌سازی با سیستم اعلان‌ها
5. پشتیبانی از تصاویر پروفایل اشخاص
### بهبودهای فنی:
1. کش کردن اطلاعات پرکاربرد
2. بهینه‌سازی کوئری‌های دیتابیس
3. پشتیبانی از pagination برای لیست‌های بزرگ
4. اضافه کردن فیلترهای پیشرفته
## عیب‌یابی
### مشکلات رایج:
1. **خطای دسترسی**: بررسی کنید که کاربر دسترسی AI داشته باشد
2. **عدم یافتن شخص**: نام یا کد را بررسی کنید
3. **خطای شبکه**: اتصال اینترنت را بررسی کنید
### لاگ‌ها:
تمام خطاها در console مرورگر و لاگ‌های سرور ثبت می‌شوند.
## پشتیبانی
برای گزارش مشکلات یا درخواست ویژگی‌های جدید، لطفاً با تیم توسعه تماس بگیرید.

View file

@ -1,8 +1,3 @@
# توقف فعالیت در گیت‌هاب به دلیل نگرانی‌های اخلاقی
ما به دلیل استفاده مایکروسافت از هوش مصنوعی در تولید سلاح‌های نظامی و آموزش مدل‌های هوش مصنوعی با داده‌های غیرنظامیان، تصمیم گرفتیم تمام فعالیت‌های خود را در پلتفرم گیت‌هاب متوقف کنیم. این تصمیم به منظور پایبندی به اصول اخلاقی و مسئولیت اجتماعی اتخاذ شده است.
برای دسترسی به سورس‌کدها و مشارکت در پروژه‌های ما، لطفاً به وب‌سایت رسمی ما به آدرس [source.hesabix.ir](https://source.hesabix.ir) مراجعه کنید.
با تشکر از حمایت و همراهی شما.
# حسابیکس - نرم‌افزار حسابداری متن‌باز
<img src="https://hesabix.ir/favicon/favicon.svg" alt="Hesabix Logo" width="100" height="100" />

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

@ -8,7 +8,7 @@ doctrine:
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
controller_resolver:
auto_mapping: true
auto_mapping: false
mappings:
App:
is_bundle: false

View file

@ -21,6 +21,9 @@ framework:
#esi: true
#fragments: true
http_client:
default_options:
timeout: 30
php_errors:
log: true

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
@ -48,6 +49,14 @@ security:
# Note: Only the *first* access control that matches will be used
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
@ -40,6 +56,10 @@ services:
- '../src/Entity/'
- '../src/Kernel.php'
App\Controller\System\DebugController:
arguments:
$kernelLogsDir: '%kernel.logs_dir%'
doctrine.orm.default_attribute_driver:
class: Doctrine\ORM\Mapping\Driver\AttributeDriver
arguments:
@ -57,6 +77,13 @@ services:
tags:
- { name: kernel.event_listener, event: kernel.exception }
App\EventListener\BankAccountListener:
arguments:
$bankAccountService: '@App\Service\BankAccountService'
$entityManager: '@doctrine.orm.default_entity_manager'
tags:
- { name: doctrine.event_listener, event: postLoad, priority: 100 }
App\Security\AuthenticationFailureHandler:
arguments:
$captchaService: '@App\Service\CaptchaService'
@ -97,6 +124,70 @@ services:
tags: ['twig.extension']
App\Cog\PersonService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
App\Service\AGI\Promps\AccountingDocPromptService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
App\Service\AGI\Promps\BasePromptService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$access: '@App\Service\Access'
App\Service\AGI\Promps\PromptService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$personPromptService: '@App\Service\AGI\Promps\PersonPromptService'
$basePromptService: '@App\Service\AGI\Promps\BasePromptService'
$inventoryPromptService: '@App\Service\AGI\Promps\InventoryPromptService'
$bankPromptService: '@App\Service\AGI\Promps\BankPromptService'
$accountingDocPromptService: '@App\Service\AGI\Promps\AccountingDocPromptService'
App\Cog\AccountingDocService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
App\Cog\TicketService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$explore: '@App\Service\Explore'
$jdate: '@Jdate'
$registryMGR: '@registryMGR'
$sms: '@SMS'
$uploadDirectory: '%SupportFilesDir%'
App\Service\Explore: ~
App\AiTool\AccountingDocService:
arguments:
$em: '@doctrine.orm.entity_manager'
$cogAccountingDocService: '@App\Cog\AccountingDocService'
App\AiTool\TicketService:
arguments:
$em: '@doctrine.orm.entity_manager'
$cogTicketService: '@App\Cog\TicketService'
App\Service\AGI\AGIService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$registryMGR: '@registryMGR'
$log: '@Log'
$provider: '@Provider'
$promptService: '@App\Service\AGI\Promps\PromptService'
$httpClient: '@http_client'
$httpKernel: '@kernel'
$explore: '@App\Service\Explore'
$jdate: '@Jdate'
$sms: '@SMS'
$uploadDirectory: '%SupportFilesDir%'
# سرویس بستن سال مالی
App\Service\CloseYearService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$logService: '@Log'
$provider: '@Provider'
$params: '@parameter_bag'

View file

@ -14,24 +14,89 @@ final class Version20241201000000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create postal_code_inquiry table';
return 'Create chat tables';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE postal_code_inquiry (
// Create chat_channel table
$this->addSql('CREATE TABLE chat_channel (
id INT AUTO_INCREMENT NOT NULL,
postal_code VARCHAR(10) NOT NULL,
address_data JSON NOT NULL,
name VARCHAR(255) NOT NULL,
description LONGTEXT DEFAULT NULL,
channel_id VARCHAR(50) NOT NULL,
is_public TINYINT(1) NOT NULL,
is_active TINYINT(1) NOT NULL,
created_by_id INT NOT NULL,
created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
updated_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
UNIQUE INDEX UNIQ_POSTAL_CODE (postal_code),
updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\',
avatar VARCHAR(255) DEFAULT NULL,
message_count INT NOT NULL,
last_message_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\',
UNIQUE INDEX UNIQ_CHANNEL_ID (channel_id),
INDEX IDX_CHANNEL_CREATED_BY (created_by_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
// Create chat_channel_member table
$this->addSql('CREATE TABLE chat_channel_member (
id INT AUTO_INCREMENT NOT NULL,
channel_id INT NOT NULL,
user_id INT NOT NULL,
is_admin TINYINT(1) NOT NULL,
is_active TINYINT(1) NOT NULL,
joined_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
last_seen_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\',
unread_count INT NOT NULL,
INDEX IDX_MEMBER_CHANNEL (channel_id),
INDEX IDX_MEMBER_USER (user_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
// Create chat_message table
$this->addSql('CREATE TABLE chat_message (
id INT AUTO_INCREMENT NOT NULL,
channel_id INT NOT NULL,
sender_id INT NOT NULL,
content LONGTEXT NOT NULL,
message_type VARCHAR(20) NOT NULL,
sent_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
edited_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\',
is_edited TINYINT(1) NOT NULL,
is_deleted TINYINT(1) NOT NULL,
quoted_message_id INT DEFAULT NULL,
attachments JSON DEFAULT NULL,
reactions JSON DEFAULT NULL,
reply_count INT NOT NULL,
view_count INT NOT NULL,
INDEX IDX_MESSAGE_CHANNEL (channel_id),
INDEX IDX_MESSAGE_SENDER (sender_id),
INDEX IDX_MESSAGE_QUOTED (quoted_message_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
// Add foreign key constraints
$this->addSql('ALTER TABLE chat_channel ADD CONSTRAINT FK_CHANNEL_CREATED_BY FOREIGN KEY (created_by_id) REFERENCES user (id)');
$this->addSql('ALTER TABLE chat_channel_member ADD CONSTRAINT FK_MEMBER_CHANNEL FOREIGN KEY (channel_id) REFERENCES chat_channel (id)');
$this->addSql('ALTER TABLE chat_channel_member ADD CONSTRAINT FK_MEMBER_USER FOREIGN KEY (user_id) REFERENCES user (id)');
$this->addSql('ALTER TABLE chat_message ADD CONSTRAINT FK_MESSAGE_CHANNEL FOREIGN KEY (channel_id) REFERENCES chat_channel (id)');
$this->addSql('ALTER TABLE chat_message ADD CONSTRAINT FK_MESSAGE_SENDER FOREIGN KEY (sender_id) REFERENCES user (id)');
$this->addSql('ALTER TABLE chat_message ADD CONSTRAINT FK_MESSAGE_QUOTED FOREIGN KEY (quoted_message_id) REFERENCES chat_message (id)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE postal_code_inquiry');
// Drop foreign key constraints
$this->addSql('ALTER TABLE chat_message DROP FOREIGN KEY FK_MESSAGE_QUOTED');
$this->addSql('ALTER TABLE chat_message DROP FOREIGN KEY FK_MESSAGE_SENDER');
$this->addSql('ALTER TABLE chat_message DROP FOREIGN KEY FK_MESSAGE_CHANNEL');
$this->addSql('ALTER TABLE chat_channel_member DROP FOREIGN KEY FK_MEMBER_USER');
$this->addSql('ALTER TABLE chat_channel_member DROP FOREIGN KEY FK_MEMBER_CHANNEL');
$this->addSql('ALTER TABLE chat_channel DROP FOREIGN KEY FK_CHANNEL_CREATED_BY');
// Drop tables
$this->addSql('DROP TABLE chat_message');
$this->addSql('DROP TABLE chat_channel_member');
$this->addSql('DROP TABLE chat_channel');
}
}

View file

@ -0,0 +1,29 @@
<?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 Version20241201000001 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add customCode column to commodity table';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE commodity ADD customCode TINYINT(1) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE commodity DROP customCode');
}
}

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

@ -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 Version20250804133410 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('ALTER TABLE commodity CHANGE code code VARCHAR(255) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE commodity CHANGE code code BIGINT NOT NULL');
}
}

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250809103000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add copy_count to custom_invoice_template';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE custom_invoice_template ADD copy_count INT NOT NULL DEFAULT 0');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE custom_invoice_template DROP COLUMN copy_count');
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250809112000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add template relations to print_options for sell/buy/rfbuy/rfsell';
}
public function up(Schema $schema): void
{
// this migration is auto-generated, adjust table names if needed
$this->addSql('ALTER TABLE print_options ADD sell_template_id INT DEFAULT NULL, ADD buy_template_id INT DEFAULT NULL, ADD rfbuy_template_id INT DEFAULT NULL, ADD rfsell_template_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE print_options ADD CONSTRAINT FK_PRINT_OPTIONS_SELL_TEMPLATE FOREIGN KEY (sell_template_id) REFERENCES custom_invoice_template (id) ON DELETE SET NULL');
$this->addSql('ALTER TABLE print_options ADD CONSTRAINT FK_PRINT_OPTIONS_BUY_TEMPLATE FOREIGN KEY (buy_template_id) REFERENCES custom_invoice_template (id) ON DELETE SET NULL');
$this->addSql('ALTER TABLE print_options ADD CONSTRAINT FK_PRINT_OPTIONS_RFBUY_TEMPLATE FOREIGN KEY (rfbuy_template_id) REFERENCES custom_invoice_template (id) ON DELETE SET NULL');
$this->addSql('ALTER TABLE print_options ADD CONSTRAINT FK_PRINT_OPTIONS_RFSELL_TEMPLATE FOREIGN KEY (rfsell_template_id) REFERENCES custom_invoice_template (id) ON DELETE SET NULL');
$this->addSql('CREATE INDEX IDX_PRINT_OPTIONS_SELL_TEMPLATE ON print_options (sell_template_id)');
$this->addSql('CREATE INDEX IDX_PRINT_OPTIONS_BUY_TEMPLATE ON print_options (buy_template_id)');
$this->addSql('CREATE INDEX IDX_PRINT_OPTIONS_RFBUY_TEMPLATE ON print_options (rfbuy_template_id)');
$this->addSql('CREATE INDEX IDX_PRINT_OPTIONS_RFSELL_TEMPLATE ON print_options (rfsell_template_id)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE print_options DROP FOREIGN KEY FK_PRINT_OPTIONS_SELL_TEMPLATE');
$this->addSql('ALTER TABLE print_options DROP FOREIGN KEY FK_PRINT_OPTIONS_BUY_TEMPLATE');
$this->addSql('ALTER TABLE print_options DROP FOREIGN KEY FK_PRINT_OPTIONS_RFBUY_TEMPLATE');
$this->addSql('ALTER TABLE print_options DROP FOREIGN KEY FK_PRINT_OPTIONS_RFSELL_TEMPLATE');
$this->addSql('DROP INDEX IDX_PRINT_OPTIONS_SELL_TEMPLATE ON print_options');
$this->addSql('DROP INDEX IDX_PRINT_OPTIONS_BUY_TEMPLATE ON print_options');
$this->addSql('DROP INDEX IDX_PRINT_OPTIONS_RFBUY_TEMPLATE ON print_options');
$this->addSql('DROP INDEX IDX_PRINT_OPTIONS_RFSELL_TEMPLATE ON print_options');
$this->addSql('ALTER TABLE print_options DROP sell_template_id, DROP buy_template_id, DROP rfbuy_template_id, DROP rfsell_template_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 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

@ -0,0 +1,37 @@
<?php
namespace App\AiTool;
use Doctrine\ORM\EntityManagerInterface;
use App\Cog\AccountingDocService as CogAccountingDocService;
class AccountingDocService
{
private EntityManagerInterface $em;
private CogAccountingDocService $cogAccountingDocService;
public function __construct(EntityManagerInterface $em, CogAccountingDocService $cogAccountingDocService)
{
$this->em = $em;
$this->cogAccountingDocService = $cogAccountingDocService;
}
/**
* جست‌وجوی ردیف‌های اسناد حسابداری برای ابزار هوش مصنوعی
*/
public function searchRowsAi(array $params, $acc = null): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
return $this->cogAccountingDocService->searchRows($params, $acc);
} catch (\Exception $e) {
return [
'error' => 'خطا در جست‌وجوی ردیف‌های اسناد: ' . $e->getMessage()
];
}
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\AiTool;
use Doctrine\ORM\EntityManagerInterface;
use App\Cog\CommodityService as CogCommodityService;
class CommodityService
{
private EntityManagerInterface $em;
private CogCommodityService $cogCommodityService;
public function __construct(EntityManagerInterface $em, CogCommodityService $cogCommodityService)
{
$this->em = $em;
$this->cogCommodityService = $cogCommodityService;
}
/**
* افزودن یا ویرایش کالا برای ابزار هوش مصنوعی
*/
public function addOrUpdateCommodityAi(array $params, $acc = null, $code = 0): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
return $this->cogCommodityService->addOrUpdateCommodity($params, $acc, $code ?? ($params['code'] ?? 0));
} catch (\Exception $e) {
return [
'error' => 'خطا در افزودن/ویرایش کالا: ' . $e->getMessage()
];
}
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace App\AiTool;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Person;
use App\Service\Explore;
class PersonService
{
private EntityManagerInterface $em;
private \App\Cog\PersonService $cogPersonService;
public function __construct(EntityManagerInterface $em, \App\Cog\PersonService $cogPersonService)
{
$this->em = $em;
$this->cogPersonService = $cogPersonService;
}
/**
* دریافت اطلاعات یک شخص بر اساس کد و اطلاعات دسترسی
*/
public function getPersonInfoByCode($code, $acc): array
{
if (!$code) {
return [
'error' => 'کد شخص الزامی است'
];
}
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// فقط کد را به سرویس Cog پاس بده
return $this->cogPersonService->getPersonInfo($code, $acc);
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت اطلاعات شخص: ' . $e->getMessage()
];
}
}
/**
* دریافت لیست اشخاص با فیلتر و صفحه‌بندی برای ابزار هوش مصنوعی
*/
public function getPersonsListAi(array $params, $acc = null): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
return $this->cogPersonService->getPersonsList($params, $acc);
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت لیست اشخاص: ' . $e->getMessage()
];
}
}
/**
* افزودن یا ویرایش شخص برای ابزار هوش مصنوعی
*/
public function addOrUpdatePersonAi(array $params, $acc = null, $code = 0): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
return $this->cogPersonService->addOrUpdatePerson($params, $acc, $code ?? ($params['code'] ?? 0));
} catch (\Exception $e) {
return [
'error' => 'خطا در افزودن/ویرایش شخص: ' . $e->getMessage()
];
}
}
}

View file

@ -0,0 +1,270 @@
<?php
namespace App\AiTool;
use App\Cog\TicketService as CogTicketService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class TicketService
{
private EntityManagerInterface $em;
private CogTicketService $cogTicketService;
public function __construct(EntityManagerInterface $em, CogTicketService $cogTicketService)
{
$this->em = $em;
$this->cogTicketService = $cogTicketService;
}
/**
* دریافت لیست تیکت‌ها برای ابزار هوش مصنوعی
*/
public function getTicketsListAi(array $params, $acc = null): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// اینجا باید منطق دریافت لیست تیکت‌ها پیاده‌سازی شود
// فعلاً یک پیام موقت برمی‌گردانیم
return [
'error' => 'این قابلیت در حال توسعه است'
];
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت لیست تیکت‌ها: ' . $e->getMessage()
];
}
}
/**
* دریافت اطلاعات تیکت بر اساس کد
*/
public function getTicketInfoByCode($code, $acc): array
{
if (!$code) {
return [
'error' => 'کد تیکت الزامی است'
];
}
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// اینجا باید منطق دریافت اطلاعات تیکت پیاده‌سازی شود
return [
'error' => 'این قابلیت در حال توسعه است'
];
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت اطلاعات تیکت: ' . $e->getMessage()
];
}
}
/**
* افزودن یا ویرایش تیکت برای ابزار هوش مصنوعی
*/
public function addOrUpdateTicketAi(array $params, $acc = null, $code = 0): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// اینجا باید منطق افزودن/ویرایش تیکت پیاده‌سازی شود
return [
'error' => 'این قابلیت در حال توسعه است'
];
} catch (\Exception $e) {
return [
'error' => 'خطا در افزودن/ویرایش تیکت: ' . $e->getMessage()
];
}
}
/**
* پاسخ به تیکت برای ابزار هوش مصنوعی
*/
public function replyToTicketAi(array $params, $acc = null): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// اینجا باید منطق پاسخ به تیکت پیاده‌سازی شود
return [
'error' => 'این قابلیت در حال توسعه است'
];
} catch (\Exception $e) {
return [
'error' => 'خطا در پاسخ به تیکت: ' . $e->getMessage()
];
}
}
/**
* Get the available tools for ticket management
* @return array
*/
public function getTools(): array
{
$tools = [];
$tools[] = [
'type' => 'function',
'function' => [
'name' => 'get_user_tickets',
'description' => 'Retrieve a list of tickets for the current user',
'parameters' => [
'type' => 'object',
'properties' => [
'user' => [
'type' => 'object',
'description' => 'The user object'
]
],
'required' => ['user']
]
]
];
$tools[] = [
'type' => 'function',
'function' => [
'name' => 'create_or_update_ticket',
'description' => 'Create a new ticket or update an existing one',
'parameters' => [
'type' => 'object',
'properties' => [
'params' => [
'type' => 'object',
'description' => 'The ticket parameters',
'properties' => [
'subject' => [
'type' => 'string',
'description' => 'The subject/title of the ticket'
],
'message' => [
'type' => 'string',
'description' => 'The ticket message or description'
],
'priority' => [
'type' => 'string',
'description' => 'Ticket priority level',
'enum' => ['low', 'medium', 'high']
],
'department' => [
'type' => 'string',
'description' => 'The department this ticket belongs to',
'enum' => ['technical', 'financial', 'general']
]
],
'required' => ['subject', 'message']
],
'files' => [
'type' => 'array',
'description' => 'Array of file attachments',
'items' => [
'type' => 'object',
'description' => 'File object'
]
],
'user' => [
'type' => 'object',
'description' => 'The user object'
],
'id' => [
'type' => 'string',
'description' => 'Ticket ID (required for updates, omit for new tickets)'
]
],
'required' => ['params', 'files', 'user']
]
]
];
$tools[] = [
'type' => 'function',
'function' => [
'name' => 'get_ticket_details',
'description' => 'Get detailed information about a specific ticket including its conversation history',
'parameters' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'string',
'description' => 'The unique identifier of the ticket'
],
'user' => [
'type' => 'object',
'description' => 'The user object'
]
],
'required' => ['id', 'user']
]
]
];
return $tools;
}
/**
* دریافت لیست تیکت‌های کاربر
*/
public function getUserTickets(UserInterface $user): array
{
try {
return $this->cogTicketService->getUserTickets($user);
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت لیست تیکت‌ها: ' . $e->getMessage()
];
}
}
/**
* ایجاد یا به‌روزرسانی تیکت
*/
public function createOrUpdateTicket(array $params, array $files, UserInterface $user, string $id = ''): array
{
try {
return $this->cogTicketService->createOrUpdateTicket($params, $files, $user, $id);
} catch (\Exception $e) {
return [
'error' => 'خطا در ایجاد/به‌روزرسانی تیکت: ' . $e->getMessage()
];
}
}
/**
* دریافت جزئیات تیکت و پاسخ‌های آن
*/
public function getTicketDetails(string $id, UserInterface $user): array
{
if (!$id) {
return [
'error' => 'شناسه تیکت الزامی است'
];
}
try {
return $this->cogTicketService->getTicketDetails($id, $user);
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت جزئیات تیکت: ' . $e->getMessage()
];
}
}
}

View file

@ -0,0 +1,244 @@
<?php
namespace App\Cog;
use Doctrine\ORM\EntityManagerInterface;
class AccountingDocService
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* جست‌وجوی ردیف‌های اسناد حسابداری بر اساس نوع و شناسه
* @param array $params
* @param array $acc
* @return array
*/
public function searchRows(array $params, array $acc): array
{
$em = $this->entityManager;
$data = [];
if (!isset($params['type'])) {
return ['error' => 'نوع (type) الزامی است'];
}
$roll = '';
if ($params['type'] == 'person')
$roll = 'person';
if ($params['type'] == 'person_receive' || $params['type'] == 'person_send')
$roll = 'person';
elseif ($params['type'] == 'sell_receive')
$roll = 'sell';
elseif ($params['type'] == 'bank')
$roll = 'banks';
elseif ($params['type'] == 'buy_send')
$roll = 'buy';
elseif ($params['type'] == 'transfer')
$roll = 'bankTransfer';
elseif ($params['type'] == 'all')
$roll = 'accounting';
else
$roll = $params['type'];
// اینجا فرض می‌کنیم acc معتبر است و قبلاً بررسی شده
if ($params['type'] == 'person') {
$person = $em->getRepository(\App\Entity\Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$person)
return ['error' => 'شخص یافت نشد'];
// 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'],
'code' => $params['id'],
]);
if (!$bank)
return ['error' => 'بانک یافت نشد'];
// 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'],
'code' => $params['id'],
]);
if (!$cashdesk)
return ['error' => 'صندوق یافت نشد'];
// 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'],
'code' => $params['id'],
]);
if (!$salary)
return ['error' => 'حقوق یافت نشد'];
// 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(),
'date' => $item->getDoc()->getDate(),
'type' => $item->getDoc()->getType(),
'ref' => $item->getRef()->getName(),
'des' => $item->getDes(),
'bs' => $item->getBs(),
'bd' => $item->getBd(),
'code' => $item->getDoc()->getCode(),
'submitter' => $item->getDoc()->getSubmitter()->getFullName(),
'settlement' => $settlement, // ستون تشخیص
'balance' => $runningBalance // ستون باقی‌مانده
];
$dataTemp[] = $temp;
}
// معکوس کردن ترتیب برای نمایش جدیدترین‌ها اول
return array_reverse($dataTemp);
}
}

View file

@ -0,0 +1,146 @@
<?php
namespace App\Cog;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Commodity;
use App\Entity\CommodityUnit;
use App\Entity\CommodityCat;
use App\Entity\PriceList;
use App\Entity\PriceListDetail;
class CommodityService
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* افزودن یا ویرایش کالا/خدمات
* @param array $params
* @param array $acc
* @param int|string $code
* @return array
*/
public function addOrUpdateCommodity(array $params, array $acc, $code = 0): array
{
$em = $this->entityManager;
if (!isset($params['name']) || trim($params['name']) === '')
return ['result' => -1, 'error' => 'نام کالا الزامی است'];
if ($code == 0) {
// افزودن کالای جدید
$data = $em->getRepository(Commodity::class)->findOneBy([
'name' => $params['name'],
'bid' => $acc['bid']
]);
if (!$data) {
$data = new Commodity();
// بررسی کد سفارشی
if (isset($params['customCode']) && $params['customCode'] === true && isset($params['code'])) {
// بررسی تکراری نبودن کد سفارشی
$existingCommodity = $em->getRepository(Commodity::class)->findOneBy([
'code' => $params['code'],
'bid' => $acc['bid']
]);
if ($existingCommodity) {
return ['result' => 2, 'error' => 'کد کالا تکراری است'];
}
$data->setCode($params['code']);
$data->setCustomCode(true);
} else {
// کد اتوماتیک
$data->setCode((new \App\Service\Provider($em))->getAccountingCode($acc['bid'], 'Commodity'));
$data->setCustomCode(false);
}
}
} else {
// ویرایش کالای موجود
$data = $em->getRepository(Commodity::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
if (!$data)
return ['result' => -2, 'error' => 'کالا یافت نشد'];
// بررسی کد سفارشی در زمان ویرایش
if (isset($params['customCode']) && $params['customCode'] === true && isset($params['code'])) {
// بررسی تکراری نبودن کد سفارشی (به جز خود کالا)
$existingCommodity = $em->getRepository(Commodity::class)->findOneBy([
'code' => $params['code'],
'bid' => $acc['bid']
]);
if ($existingCommodity && $existingCommodity->getId() !== $data->getId()) {
return ['result' => 2, 'error' => 'کد کالا تکراری است'];
}
$data->setCode($params['code']);
$data->setCustomCode(true);
} elseif (isset($params['customCode']) && $params['customCode'] === false) {
// تغییر به کد اتوماتیک
$data->setCode((new \App\Service\Provider($em))->getAccountingCode($acc['bid'], 'Commodity'));
$data->setCustomCode(false);
}
}
$unit = null;
if (!isset($params['unit']))
$unit = $em->getRepository(CommodityUnit::class)->findAll()[0];
else
$unit = $em->getRepository(CommodityUnit::class)->findOneBy(['name' => $params['unit']]);
if (!$unit)
return ['result' => -3, 'error' => 'واحد کالا یافت نشد'];
$data->setUnit($unit);
$data->setBid($acc['bid']);
$data->setName($params['name']);
$data->setKhadamat($params['khadamat'] ?? false);
$data->setWithoutTax($params['withoutTax'] ?? false);
if (isset($params['des'])) $data->setDes($params['des']);
if (isset($params['priceSell'])) $data->setPriceSell($params['priceSell']);
if (isset($params['priceBuy'])) $data->setPriceBuy($params['priceBuy']);
if (isset($params['commodityCountCheck'])) $data->setCommodityCountCheck($params['commodityCountCheck']);
if (isset($params['barcodes'])) $data->setBarcodes($params['barcodes']);
if (isset($params['taxCode'])) $data->setTaxCode($params['taxCode']);
if (isset($params['taxType'])) $data->setTaxType($params['taxType']);
if (isset($params['taxUnit'])) $data->setTaxUnit($params['taxUnit']);
if (isset($params['minOrderCount'])) $data->setMinOrderCount($params['minOrderCount']);
if (isset($params['speedAccess'])) $data->setSpeedAccess($params['speedAccess']);
if (isset($params['dayLoading'])) $data->setDayLoading($params['dayLoading']);
if (isset($params['orderPoint'])) $data->setOrderPoint($params['orderPoint']);
// دسته‌بندی
if (isset($params['cat']) && $params['cat'] != '') {
$cat = is_array($params['cat']) ? $em->getRepository(CommodityCat::class)->find($params['cat']['id']) : $em->getRepository(CommodityCat::class)->find($params['cat']);
if ($cat && $cat->getBid() == $acc['bid']) {
$data->setCat($cat);
}
}
$em->persist($data);
// قیمت‌ها
if (isset($params['prices'])) {
foreach ($params['prices'] as $item) {
$priceList = $em->getRepository(PriceList::class)->findOneBy([
'bid' => $acc['bid'],
'id' => $item['list']['id']
]);
if ($priceList) {
$detail = $em->getRepository(PriceListDetail::class)->findOneBy([
'list' => $priceList,
'commodity' => $data
]);
if (!$detail) $detail = new PriceListDetail();
$detail->setList($priceList);
$detail->setCommodity($data);
$detail->setPriceSell($item['priceSell']);
$detail->setPriceBuy(0);
$detail->setMoney($acc['money']);
$em->persist($detail);
}
}
}
$em->flush();
return ['Success' => true, 'result' => 1, 'code' => $data->getId()];
}
}

View file

@ -0,0 +1,182 @@
<?php
namespace App\Cog;
use App\Entity\Hook;
use App\Entity\Business;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
class HookService
{
private $entityManager;
private $httpClient;
public function __construct(
EntityManagerInterface $entityManager,
HttpClientInterface $httpClient
) {
$this->entityManager = $entityManager;
$this->httpClient = $httpClient;
}
/**
* دریافت تمام هوک‌های یک کسب و کار
*/
public function getHooksByBusiness(Business $business): array
{
return $this->entityManager->getRepository(Hook::class)->findBy([
'bid' => $business
]);
}
/**
* ارسال داده به تمام هوک‌های یک کسب و کار
*/
public function sendToHooks(Business $business, array $data, string $event = 'general'): array
{
$hooks = $this->getHooksByBusiness($business);
$results = [];
foreach ($hooks as $hook) {
$result = $this->sendToHook($hook, $data, $event);
$results[] = [
'hook_id' => $hook->getId(),
'url' => $hook->getUrl(),
'success' => $result['success'],
'response' => $result['response'],
'error' => $result['error'] ?? null
];
}
return $results;
}
/**
* ارسال داده به یک هوک خاص
*/
public function sendToHook(Hook $hook, array $data, string $event = 'general'): array
{
$url = $hook->getUrl();
$password = $hook->getPassword();
// آماده‌سازی داده‌های ارسالی
$payload = [
'event' => $event,
'timestamp' => time(),
'data' => $data,
'password' => $password
];
try {
$response = $this->httpClient->request('POST', $url, [
'headers' => [
'Content-Type' => 'application/json',
'User-Agent' => 'Hesabix-Hook-Service/1.0'
],
'json' => $payload,
'timeout' => 10,
'max_redirects' => 3
]);
$statusCode = $response->getStatusCode();
$content = $response->getContent(false);
if ($statusCode >= 200 && $statusCode < 300) {
return [
'success' => true,
'response' => json_decode($content, true) ?: $content,
'status_code' => $statusCode
];
} else {
return [
'success' => false,
'error' => "HTTP Error: {$statusCode}",
'response' => $content,
'status_code' => $statusCode
];
}
} catch (\Throwable $e) {
return [
'success' => false,
'error' => $e->getMessage(),
'status_code' => 0
];
}
}
/**
* ارسال اعلان تغییر شخص
*/
public function sendPersonChange(Business $business, array $personData, string $action = 'update'): array
{
$data = [
'action' => $action,
'person' => $personData,
'business_id' => $business->getId(),
'business_name' => $business->getName()
];
return $this->sendToHooks($business, $data, 'person_change');
}
/**
* ارسال اعلان تغییر کالا
*/
public function sendCommodityChange(Business $business, array $commodityData, string $action = 'update'): array
{
$data = [
'action' => $action,
'commodity' => $commodityData,
'business_id' => $business->getId(),
'business_name' => $business->getName()
];
return $this->sendToHooks($business, $data, 'commodity_change');
}
/**
* ارسال اعلان تغییر فاکتور
*/
public function sendInvoiceChange(Business $business, array $invoiceData, string $action = 'update'): array
{
$data = [
'action' => $action,
'invoice' => $invoiceData,
'business_id' => $business->getId(),
'business_name' => $business->getName()
];
return $this->sendToHooks($business, $data, 'invoice_change');
}
/**
* ارسال اعلان مالیاتی
*/
public function sendTaxNotification(Business $business, array $taxData, string $action = 'send'): array
{
$data = [
'action' => $action,
'tax_invoice' => $taxData,
'business_id' => $business->getId(),
'business_name' => $business->getName()
];
return $this->sendToHooks($business, $data, 'tax_notification');
}
/**
* تست اتصال به هوک
*/
public function testHook(Hook $hook): array
{
$testData = [
'test' => true,
'message' => 'تست اتصال هوک',
'timestamp' => time()
];
return $this->sendToHook($hook, $testData, 'test');
}
}

View file

@ -7,7 +7,6 @@ use App\Entity\Person;
use App\Entity\PersonType;
use App\Entity\HesabdariRow;
use App\Service\Explore;
use App\Service\Access;
/**
* سرویس مدیریت اشخاص
@ -17,15 +16,13 @@ use App\Service\Access;
class PersonService
{
private EntityManagerInterface $entityManager;
private Access $access;
/**
* سازنده سرویس
*/
public function __construct(EntityManagerInterface $entityManager, Access $access)
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
$this->access = $access;
}
/**
@ -72,7 +69,291 @@ class PersonService
$response['bs'] = $bs;
$response['bd'] = $bd;
$response['balance'] = $bs - $bd;
return $response;
}
/**
* دریافت لیست اشخاص با فیلتر، جست‌وجو و صفحه‌بندی
*
* @param array $params پارامترهای جست‌وجو و فیلتر
* @param array $acc اطلاعات دسترسی
* @return array
*/
public function getPersonsList(array $params, array $acc): array
{
$page = $params['page'] ?? 1;
$itemsPerPage = $params['itemsPerPage'] ?? 10;
$search = $params['search'] ?? '';
$types = $params['types'] ?? null;
$transactionFilters = $params['transactionFilters'] ?? null;
$sortBy = $params['sortBy'] ?? null;
$queryBuilder = $this->entityManager->getRepository(Person::class)
->createQueryBuilder('p')
->where('p.bid = :bid')
->setParameter('bid', $acc['bid']);
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 OR p.paymentId LIKE :search')
->setParameter('search', "%$search%");
}
if ($types && !empty($types)) {
$queryBuilder->leftJoin('p.type', 't')
->andWhere('t.code IN (:types)')
->setParameter('types', $types);
}
// بررسی اینکه آیا سورت روی فیلدهای محاسبه‌شده است
$hasCalculatedSort = false;
$calculatedSortField = null;
$calculatedSortOrder = null;
if ($sortBy && is_array($sortBy) && !empty($sortBy)) {
foreach ($sortBy as $sort) {
if (isset($sort['key']) && in_array($sort['key'], ['bs', 'bd', 'balance'])) {
$hasCalculatedSort = true;
$calculatedSortField = $sort['key'];
$calculatedSortOrder = $sort['order'];
break;
}
}
}
// همیشه ابتدا همه اشخاص را دریافت کن
$persons = $queryBuilder
->select('p')
->getQuery()
->getResult();
// محاسبه تعداد کل آیتم‌ها
$totalQueryBuilder = clone $queryBuilder;
try {
$totalItems = $totalQueryBuilder
->select('COUNT(p.id)')
->getQuery()
->getSingleScalarResult();
} catch (\Doctrine\ORM\NoResultException $e) {
$totalItems = 0;
}
// اگر هیچ آیتمی وجود ندارد، خالی برگردان
if ($totalItems == 0) {
return [
'items' => [],
'total' => 0,
'unfilteredTotal' => 0,
];
}
// ابتدا همه اشخاص را با تراز محاسبه کن
$allPersonsWithBalance = [];
foreach ($persons as $person) {
$rows = $this->entityManager->getRepository(HesabdariRow::class)->findBy([
'person' => $person,
'bid' => $acc['bid']
]);
$bs = 0;
$bd = 0;
foreach ($rows as $row) {
$doc = $row->getDoc();
if ($doc && $doc->getMoney() && $doc->getYear() &&
$doc->getMoney()->getId() == $acc['money']->getId() &&
$doc->getYear()->getId() == $acc['year']->getId()) {
$bs += (float) $row->getBs();
$bd += (float) $row->getBd();
}
}
$balance = $bs - $bd;
$result = Explore::ExplorePerson($person, $this->entityManager->getRepository(PersonType::class)->findAll());
$result['bs'] = $bs;
$result['bd'] = $bd;
$result['balance'] = $balance;
$allPersonsWithBalance[] = $result;
}
// اگر سورت روی فیلدهای محاسبه‌شده است، ابتدا سورت کن
if ($hasCalculatedSort && $calculatedSortField && $calculatedSortOrder) {
usort($allPersonsWithBalance, function($a, $b) use ($calculatedSortField, $calculatedSortOrder) {
$aVal = $a[$calculatedSortField] ?? 0;
$bVal = $b[$calculatedSortField] ?? 0;
if ($calculatedSortOrder === 'ASC') {
return $aVal <=> $bVal;
} else {
return $bVal <=> $aVal;
}
});
}
// سپس فیلترهای تراکنش را اعمال کن
$filteredPersons = [];
foreach ($allPersonsWithBalance as $person) {
$include = true;
if ($transactionFilters && !empty($transactionFilters)) {
$include = false;
$balance = $person['balance'];
if (in_array('debtors', $transactionFilters) && $balance < 0) {
$include = true;
}
if (in_array('creditors', $transactionFilters) && $balance > 0) {
$include = true;
}
if (in_array('zero', $transactionFilters) && $balance == 0) {
$include = true;
}
}
if ($include) {
$filteredPersons[] = $person;
}
}
// محاسبه تعداد کل آیتم‌های فیلترشده
$filteredTotal = count($filteredPersons);
// اعمال صفحه‌بندی
$response = array_slice($filteredPersons, ($page - 1) * $itemsPerPage, $itemsPerPage);
return [
'items' => $response,
'total' => $filteredTotal,
'unfilteredTotal' => $totalItems,
];
}
/**
* افزودن یا ویرایش شخص
* @param array $params
* @param array $acc
* @param int|string $code
* @return array
*/
public function addOrUpdatePerson(array $params, array $acc, $code = 0): array
{
$em = $this->entityManager;
if (!isset($params['nikename']) || trim($params['nikename']) === '')
return ['result' => -1, 'error' => 'نام مستعار الزامی است'];
if ($code == 0) {
$person = $em->getRepository(\App\Entity\Person::class)->findOneBy([
'nikename' => $params['nikename'],
'bid' => $acc['bid']
]);
if (!$person) {
$person = new \App\Entity\Person();
$maxAttempts = 10;
$newCode = null;
for ($i = 0; $i < $maxAttempts; $i++) {
$newCode = $params['code'] ?? $code;
if (!$newCode || $newCode == 0) {
$newCode = (new \App\Service\Provider($em))->getAccountingCode($acc['bid'], 'person');
}
$exist = $em->getRepository(\App\Entity\Person::class)->findOneBy(['code' => $newCode]);
if (!$exist) break;
}
if ($newCode === null) return ['result' => -2, 'error' => 'کد جدید تولید نشد'];
$person->setCode($newCode);
}
} else {
$person = $em->getRepository(\App\Entity\Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
if (!$person) return ['result' => -3, 'error' => 'شخص یافت نشد'];
}
$person->setBid($acc['bid']);
$person->setNikename($params['nikename']);
if (isset($params['name'])) $person->setName($params['name']);
if (isset($params['birthday'])) $person->setBirthday($params['birthday']);
if (isset($params['tel'])) $person->setTel($params['tel']);
if (isset($params['speedAccess'])) $person->setSpeedAccess($params['speedAccess']);
if (isset($params['address'])) $person->setAddress($params['address']);
if (isset($params['des'])) $person->setDes($params['des']);
if (isset($params['mobile'])) $person->setMobile($params['mobile']);
if (isset($params['mobile2'])) $person->setMobile2($params['mobile2']);
if (isset($params['fax'])) $person->setFax($params['fax']);
if (isset($params['website'])) $person->setWebsite($params['website']);
if (isset($params['email'])) $person->setEmail($params['email']);
if (isset($params['postalcode'])) $person->setPostalcode($params['postalcode']);
if (isset($params['shahr'])) $person->setShahr($params['shahr']);
if (isset($params['ostan'])) $person->setOstan($params['ostan']);
if (isset($params['keshvar'])) $person->setKeshvar($params['keshvar']);
if (isset($params['sabt'])) $person->setSabt($params['sabt']);
if (isset($params['codeeghtesadi'])) $person->setCodeeghtesadi($params['codeeghtesadi']);
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']]);
if ($prelabel) $person->setPrelabel($prelabel);
} elseif ($params['prelabel'] == null) {
$person->setPrelabel(null);
}
}
// کارت‌ها
if (isset($params['accounts'])) {
foreach ($params['accounts'] as $item) {
$card = $em->getRepository(\App\Entity\PersonCard::class)->findOneBy([
'bid' => $acc['bid'],
'person' => $person,
'bank' => $item['bank']
]);
if (!$card) $card = new \App\Entity\PersonCard();
$card->setPerson($person);
$card->setBid($acc['bid']);
$card->setShabaNum($item['shabaNum']);
$card->setCardNum($item['cardNum']);
$card->setAccountNum($item['accountNum']);
$card->setBank($item['bank']);
$em->persist($card);
}
// حذف کارت‌های حذف‌شده
$accounts = $em->getRepository(\App\Entity\PersonCard::class)->findBy([
'bid' => $acc['bid'],
'person' => $person,
]);
foreach ($accounts as $item) {
$deleted = true;
foreach ($params['accounts'] as $param) {
if ($item->getBank() == $param['bank']) $deleted = false;
}
if ($deleted) $em->remove($item);
}
}
// نوع‌ها
if (isset($params['types'])) {
$types = $em->getRepository(\App\Entity\PersonType::class)->findAll();
foreach ($params['types'] as $item) {
$typeEntity = $em->getRepository(\App\Entity\PersonType::class)->findOneBy(['code' => $item['code']]);
if ($item['checked'] == true) $person->addType($typeEntity);
elseif ($item['checked'] == false) $person->removeType($typeEntity);
}
}
$em->persist($person);
$em->flush();
return ['Success' => true, 'result' => 1, 'code' => $person->getCode()];
}
}

View file

@ -0,0 +1,193 @@
<?php
namespace App\Cog;
use App\Entity\Business;
use App\Entity\Support;
use App\Service\Explore;
use App\Service\Jdate;
use App\Service\registryMGR;
use App\Service\SMS;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface;
class TicketService
{
private const ERROR_TICKET_NOT_FOUND = ['error' => 1, 'message' => 'تیکت یافت نشد.'];
private const ERROR_INVALID_PARAMS = ['error' => 999, 'message' => 'تمام موارد لازم را وارد کنید.'];
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly Explore $explore,
private readonly Jdate $jdate,
private readonly registryMGR $registryMGR,
private readonly SMS $sms,
private readonly string $uploadDirectory
) {
}
/**
* Get list of support tickets for a user
*/
public function getUserTickets(UserInterface $user): array
{
$items = $this->entityManager->getRepository(Support::class)->findBy(
['submitter' => $user, 'main' => 0],
['id' => 'DESC']
);
return array_map(function ($item) use ($user) {
return $this->explore->ExploreSupportTicket($item, $user);
}, $items);
}
/**
* Create or update a support ticket
*/
public function createOrUpdateTicket(array $params, array $files, UserInterface $user, string $id = ''): array
{
if ($id === '') {
return $this->createNewTicket($params, $files, $user);
}
return $this->replyToTicket($params, $files, $user, $id);
}
private function createNewTicket(array $params, array $files, UserInterface $user): array
{
if (!isset($params['title'], $params['body'])) {
return self::ERROR_INVALID_PARAMS;
}
$item = new Support();
$item->setBody($params['body'])
->setTitle($params['title'])
->setDateSubmit(time())
->setSubmitter($user)
->setMain(0)
->setCode($this->generateRandomString(8))
->setState('در حال پیگیری');
// چک کردن مالکیت کسب‌وکار
$this->handleBusinessOwnership($item, $params['bid'] ?? null, $user);
$this->entityManager->persist($item);
$this->entityManager->flush();
$fileName = $this->handleFileUpload($files, $item->getId());
if ($fileName) {
$item->setFileName($fileName);
$this->entityManager->persist($item);
$this->entityManager->flush();
}
$this->sms->send([$item->getId()], $this->registryMGR->get('sms', 'ticketRec'), $this->registryMGR->get('ticket', 'managerMobile'));
return [
'error' => 0,
'message' => 'ok',
'url' => $item->getId(),
'files' => $fileName
];
}
private function replyToTicket(array $params, array $files, UserInterface $user, string $id): array
{
if (!isset($params['body'])) {
return self::ERROR_INVALID_PARAMS;
}
$upper = $this->getTicket($id);
if (!$upper) {
return self::ERROR_TICKET_NOT_FOUND;
}
$item = new Support();
$item->setMain($upper->getId())
->setBody($params['body'])
->setTitle($upper->getTitle())
->setDateSubmit(time())
->setSubmitter($user)
->setState('در حال پیگیری');
$this->entityManager->persist($item);
$this->entityManager->flush();
$fileName = $this->handleFileUpload($files, $item->getId());
if ($fileName) {
$item->setFileName($fileName);
}
$this->entityManager->persist($item);
$upper->setState('در حال پیگیری');
$this->entityManager->persist($upper);
$this->entityManager->flush();
$this->sms->send([$item->getId()], $this->registryMGR->get('sms', 'ticketRec'), $this->registryMGR->get('ticket', 'managerMobile'));
return [
'error' => 0,
'message' => 'ok',
'url' => $item->getId(),
'files' => $fileName
];
}
private function handleBusinessOwnership(Support $support, ?string $businessId, UserInterface $user): void
{
if ($businessId) {
$business = $this->entityManager->getRepository(Business::class)->find($businessId);
if ($business && $business->getOwner() === $user) {
$support->setBid($business);
return;
}
}
$support->setBid(null);
}
private function getTicket(string $id): ?Support
{
return $this->entityManager->getRepository(Support::class)->find($id);
}
private function generateRandomString(int $length = 32): string
{
return substr(str_shuffle(str_repeat('23456789ABCDEFGHJKLMNPQRSTUVWXYZ', ceil($length / 32))), 1, $length);
}
private function handleFileUpload(array $files, int $ticketId): ?string
{
if (!file_exists($this->uploadDirectory)) {
mkdir($this->uploadDirectory, 0777, true);
}
if (!empty($files)) {
$file = $files[0];
$extension = $file->getClientOriginalExtension();
$fileName = $ticketId . '.' . $extension;
$file->move($this->uploadDirectory, $fileName);
return $fileName;
}
return null;
}
/**
* Get ticket details with its replies
*/
public function getTicketDetails(string $id, UserInterface $user): array
{
$ticket = $this->entityManager->getRepository(Support::class)->find($id);
if (!$ticket || $ticket->getSubmitter() !== $user) {
throw new AccessDeniedException('شما اجازه دسترسی به این تیکت را ندارید.');
}
$replies = $this->entityManager->getRepository(Support::class)->findBy(['main' => $ticket->getId()]);
$repliesArray = array_map(fn($reply) => $this->explore->ExploreSupportTicket($reply, $user), $replies);
return [
'item' => $this->explore->ExploreSupportTicket($ticket, $user),
'replays' => $repliesArray
];
}
}

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

@ -0,0 +1,87 @@
<?php
namespace App\Command;
use App\Entity\Business;
use App\Service\BankAccountService;
use Doctrine\ORM\EntityManagerInterface;
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:fix-bank-duplicate-codes',
description: 'اصلاح کدهای تکراری حساب‌های بانکی در تمام کسب و کارها'
)]
class FixBankDuplicateCodesCommand extends Command
{
private EntityManagerInterface $entityManager;
private BankAccountService $bankAccountService;
public function __construct(EntityManagerInterface $entityManager, BankAccountService $bankAccountService)
{
parent::__construct();
$this->entityManager = $entityManager;
$this->bankAccountService = $bankAccountService;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('اصلاح کدهای تکراری حساب‌های بانکی');
// دریافت تمام کسب و کارها
$businesses = $this->entityManager->getRepository(Business::class)->findAll();
$totalFixed = 0;
$totalErrors = 0;
foreach ($businesses as $business) {
$io->section("بررسی کسب و کار: {$business->getName()} (ID: {$business->getId()})");
// بررسی کدهای تکراری
$duplicates = $this->bankAccountService->checkDuplicateCodes($business);
if (empty($duplicates)) {
$io->info('هیچ کد تکراری یافت نشد');
continue;
}
$io->warning("تعداد کدهای تکراری یافت شده: " . count($duplicates));
foreach ($duplicates as $duplicate) {
$io->text("کد تکراری: {$duplicate['code']} - تعداد: {$duplicate['count']}");
foreach ($duplicate['accounts'] as $account) {
$io->text(" - حساب: {$account['name']} (ID: {$account['id']})");
}
}
// اصلاح کدهای تکراری
$fixResult = $this->bankAccountService->fixDuplicateCodes($business);
if ($fixResult['success']) {
$io->success("تعداد اصلاح شده: {$fixResult['fixed_count']}");
$totalFixed += $fixResult['fixed_count'];
} else {
$io->error("خطا در اصلاح کدهای تکراری:");
foreach ($fixResult['errors'] as $error) {
$io->text(" - {$error}");
}
$totalErrors += count($fixResult['errors']);
}
}
$io->newLine();
$io->title('نتیجه نهایی');
$io->success("کل تعداد اصلاح شده: {$totalFixed}");
if ($totalErrors > 0) {
$io->error("کل تعداد خطاها: {$totalErrors}");
}
return Command::SUCCESS;
}
}

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);
}
@ -371,66 +375,69 @@ class AdminController extends AbstractController
}
if (array_key_exists('username', $params))
$registryMGR->update('sms', 'username', $params['username']);
$registryMGR->update('sms', 'username', $params['username'] ?? '');
if (array_key_exists('password', $params))
$registryMGR->update('sms', 'password', $params['password']);
$registryMGR->update('sms', 'password', $params['password'] ?? '');
if (array_key_exists('token', $params))
$registryMGR->update('sms', 'token', $params['token']);
$registryMGR->update('sms', 'token', $params['token'] ?? '');
if (array_key_exists('walletpay', $params))
$registryMGR->update('sms', 'walletpay', $params['walletpay']);
$registryMGR->update('sms', 'walletpay', $params['walletpay'] ?? '');
if (array_key_exists('changePassword', $params))
$registryMGR->update('sms', 'changePassword', $params['changePassword']);
$registryMGR->update('sms', 'changePassword', $params['changePassword'] ?? '');
if (array_key_exists('recPassword', $params))
$registryMGR->update('sms', 'recPassword', $params['recPassword']);
$registryMGR->update('sms', 'recPassword', $params['recPassword'] ?? '');
if (array_key_exists('f2a', $params))
$registryMGR->update('sms', 'f2a', $params['f2a']);
$registryMGR->update('sms', 'f2a', $params['f2a'] ?? '');
if (array_key_exists('ticketReplay', $params))
$registryMGR->update('sms', 'ticketReplay', $params['ticketReplay']);
$registryMGR->update('sms', 'ticketReplay', $params['ticketReplay'] ?? '');
if (array_key_exists('ticketRec', $params))
$registryMGR->update('sms', 'ticketRec', $params['ticketRec']);
$registryMGR->update('sms', 'ticketRec', $params['ticketRec'] ?? '');
if (array_key_exists('fromNum', $params))
$registryMGR->update('sms', 'fromNum', $params['fromNum']);
$registryMGR->update('sms', 'fromNum', $params['fromNum'] ?? '');
if (array_key_exists('sharefaktor', $params))
$registryMGR->update('sms', 'sharefaktor', $params['sharefaktor']);
$registryMGR->update('sms', 'sharefaktor', $params['sharefaktor'] ?? '');
if (array_key_exists('plan', $params))
$registryMGR->update('sms', 'plan', $params['plan']);
$registryMGR->update('sms', 'plan', $params['plan'] ?? '');
if (array_key_exists('chequeInput', $params))
$registryMGR->update('sms', 'chequeInput', $params['chequeInput']);
$registryMGR->update('sms', 'chequeInput', $params['chequeInput'] ?? '');
if (array_key_exists('passChequeInput', $params))
$registryMGR->update('sms', 'passChequeInput', $params['passChequeInput']);
$registryMGR->update('sms', 'passChequeInput', $params['passChequeInput'] ?? '');
if (array_key_exists('rejectChequeInput', $params))
$registryMGR->update('sms', 'rejectChequeInput', $params['rejectChequeInput']);
$registryMGR->update('sms', 'rejectChequeInput', $params['rejectChequeInput'] ?? '');
if (array_key_exists('plugRepservice', $params)) {
if (array_key_exists('get', $params['plugRepservice']))
$registryMGR->update('sms', 'plugRepserviceStateGet', $params['plugRepservice']['get']);
$registryMGR->update('sms', 'plugRepserviceStateGet', $params['plugRepservice']['get'] ?? '');
if (array_key_exists('repaired', $params['plugRepservice']))
$registryMGR->update('sms', 'plugRepserviceStateRepaired', $params['plugRepservice']['repaired']);
$registryMGR->update('sms', 'plugRepserviceStateRepaired', $params['plugRepservice']['repaired'] ?? '');
if (array_key_exists('unrepaired', $params['plugRepservice']))
$registryMGR->update('sms', 'plugRepserviceStateUnrepired', $params['plugRepservice']['unrepaired']);
$registryMGR->update('sms', 'plugRepserviceStateUnrepired', $params['plugRepservice']['unrepaired'] ?? '');
if (array_key_exists('getback', $params['plugRepservice']))
$registryMGR->update('sms', 'plugRepserviceStateGetback', $params['plugRepservice']['getback']);
$registryMGR->update('sms', 'plugRepserviceStateGetback', $params['plugRepservice']['getback'] ?? '');
if (array_key_exists('creating', $params['plugRepservice']))
$registryMGR->update('sms', 'plugRepserviceStateCreating', $params['plugRepservice']['creating']);
$registryMGR->update('sms', 'plugRepserviceStateCreating', $params['plugRepservice']['creating'] ?? '');
if (array_key_exists('created', $params['plugRepservice']))
$registryMGR->update('sms', 'plugRepserviceStateCreated', $params['plugRepservice']['created']);
$registryMGR->update('sms', 'plugRepserviceStateCreated', $params['plugRepservice']['created'] ?? '');
}
if (array_key_exists('plugAccpro', $params)) {
if (array_key_exists('sharefaktor', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproSharefaktor', $params['plugAccpro']['sharefaktor']);
$registryMGR->update('sms', 'plugAccproSharefaktor', $params['plugAccpro']['sharefaktor'] ?? '');
if (array_key_exists('storeroomSmsBarbari', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproStoreroomSmsBarbari', $params['plugAccpro']['storeroomSmsBarbari']);
$registryMGR->update('sms', 'plugAccproStoreroomSmsBarbari', $params['plugAccpro']['storeroomSmsBarbari'] ?? '');
if (array_key_exists('storeroomSmsOther', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproStoreroomSmsOther', $params['plugAccpro']['storeroomSmsOther']);
$registryMGR->update('sms', 'plugAccproStoreroomSmsOther', $params['plugAccpro']['storeroomSmsOther'] ?? '');
if (array_key_exists('chequeInput', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproChequeInput', $params['plugAccpro']['chequeInput']);
$registryMGR->update('sms', 'plugAccproChequeInput', $params['plugAccpro']['chequeInput'] ?? '');
if (array_key_exists('passChequeInput', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproPassChequeInput', $params['plugAccpro']['passChequeInput']);
$registryMGR->update('sms', 'plugAccproPassChequeInput', $params['plugAccpro']['passChequeInput'] ?? '');
if (array_key_exists('rejectChequeInput', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproRejectChequeInput', $params['plugAccpro']['rejectChequeInput']);
$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());
}
@ -469,6 +476,7 @@ class AdminController extends AbstractController
$resp['inputTokenPrice'] = $registryMGR->get('system', key: 'inputTokenPrice');
$resp['outputTokenPrice'] = $registryMGR->get('system', key: 'outputTokenPrice');
$resp['aiPrompt'] = $registryMGR->get('system', key: 'aiPrompt');
$resp['aiDebugMode'] = $registryMGR->get('system', key: 'aiDebugMode');
return $this->json($resp);
}
@ -486,41 +494,43 @@ class AdminController extends AbstractController
$item->setSiteKeywords($params['keywords']);
$item->setDiscription($params['description']);
$item->setScripts($params['scripts']);
$registryMGR->update('system', 'zarinpalKey', $params['zarinpal']);
$registryMGR->update('system', 'zarinpalKey', $params['zarinpal'] ?? '');
$item->setFooterScripts($params['footerScripts']);
$item->setAppSite($params['appSite']);
$item->setFooter($params['footer']);
$registryMGR->update('system', 'activeGateway', $params['activeGateway']);
$registryMGR->update('system', 'parsianGatewayAPI', $params['parsianGatewayAPI']);
$registryMGR->update('system', 'paypingKey', $params['paypingKey']);
$registryMGR->update('system', 'bitpayKey', $params['bitpayKey']);
$registryMGR->update('system', 'inquiryPanel', $params['inquiryPanel']);
$registryMGR->update('system', 'inquiryZohalAPIKey', $params['inquiryZohalAPIKey']);
$registryMGR->update('system', 'enablePostalCodeToAddress', $params['enablePostalCodeToAddress']);
$registryMGR->update('system', 'inquiryPanelEnable', $params['inquiryPanelEnable']);
$registryMGR->update('system', 'postalCodeToAddressFee', $params['postalCodeToAddressFee']);
$registryMGR->update('system', 'enableCardToSheba', $params['enableCardToSheba']);
$registryMGR->update('system', 'cardToShebaFee', $params['cardToShebaFee']);
$registryMGR->update('system', 'enableAccountToSheba', $params['enableAccountToSheba']);
$registryMGR->update('system', 'accountToShebaFee', $params['accountToShebaFee']);
$registryMGR->update('system', 'activeGateway', $params['activeGateway'] ?? '');
$registryMGR->update('system', 'parsianGatewayAPI', $params['parsianGatewayAPI'] ?? '');
$registryMGR->update('system', 'paypingKey', $params['paypingKey'] ?? '');
$registryMGR->update('system', 'bitpayKey', $params['bitpayKey'] ?? '');
$registryMGR->update('system', 'inquiryPanel', $params['inquiryPanel'] ?? '');
$registryMGR->update('system', 'inquiryZohalAPIKey', $params['inquiryZohalAPIKey'] ?? '');
$registryMGR->update('system', 'enablePostalCodeToAddress', $params['enablePostalCodeToAddress'] ?? '');
$registryMGR->update('system', 'inquiryPanelEnable', $params['inquiryPanelEnable'] ?? '');
$registryMGR->update('system', 'postalCodeToAddressFee', $params['postalCodeToAddressFee'] ?? '');
$registryMGR->update('system', 'enableCardToSheba', $params['enableCardToSheba'] ?? '');
$registryMGR->update('system', 'cardToShebaFee', $params['cardToShebaFee'] ?? '');
$registryMGR->update('system', 'enableAccountToSheba', $params['enableAccountToSheba'] ?? '');
$registryMGR->update('system', 'accountToShebaFee', $params['accountToShebaFee'] ?? '');
// ذخیره تنظیمات جادوگر هوش مصنوعی
if (array_key_exists('aiEnabled', $params))
$registryMGR->update('system', 'aiEnabled', $params['aiEnabled']);
$registryMGR->update('system', 'aiEnabled', $params['aiEnabled'] ?? '');
if (array_key_exists('aiAgentSource', $params))
$registryMGR->update('system', 'aiAgentSource', $params['aiAgentSource']);
$registryMGR->update('system', 'aiAgentSource', $params['aiAgentSource'] ?? '');
if (array_key_exists('aiModel', $params))
$registryMGR->update('system', 'aiModel', $params['aiModel']);
$registryMGR->update('system', 'aiModel', $params['aiModel'] ?? '');
if (array_key_exists('aiApiKey', $params))
$registryMGR->update('system', 'aiApiKey', $params['aiApiKey']);
$registryMGR->update('system', 'aiApiKey', $params['aiApiKey'] ?? '');
if (array_key_exists('localModelAddress', $params))
$registryMGR->update('system', 'localModelAddress', $params['localModelAddress']);
$registryMGR->update('system', 'localModelAddress', $params['localModelAddress'] ?? '');
if (array_key_exists('inputTokenPrice', $params))
$registryMGR->update('system', 'inputTokenPrice', $params['inputTokenPrice']);
$registryMGR->update('system', 'inputTokenPrice', $params['inputTokenPrice'] ?? '');
if (array_key_exists('outputTokenPrice', $params))
$registryMGR->update('system', 'outputTokenPrice', $params['outputTokenPrice']);
$registryMGR->update('system', 'outputTokenPrice', $params['outputTokenPrice'] ?? '');
if (array_key_exists('aiPrompt', $params))
$registryMGR->update('system', 'aiPrompt', $params['aiPrompt']);
$registryMGR->update('system', 'aiPrompt', $params['aiPrompt'] ?? '');
if (array_key_exists('aiDebugMode', $params))
$registryMGR->update('system', 'aiDebugMode', $params['aiDebugMode'] ?? '');
$entityManager->persist($item);
$entityManager->flush();
@ -600,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();
@ -721,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)
]);
}
@ -140,6 +179,7 @@ class BankController extends AbstractController
if (count_chars(trim($params['name'])) == 0)
return $this->json(['result' => 3]);
if ($code == 0) {
// بررسی وجود حساب با نام یکسان
$data = $entityManager->getRepository(BankAccount::class)->findOneBy([
'name' => $params['name'],
'bid' => $acc['bid']
@ -147,8 +187,35 @@ class BankController extends AbstractController
//check exist before
if ($data)
return $this->json(['result' => 2]);
// تولید کد یکتا
$newCode = $provider->getAccountingCode($request->headers->get('activeBid'), 'bank');
// بررسی وجود حساب با کد یکسان در همان کسب و کار
$existingBankWithCode = $entityManager->getRepository(BankAccount::class)->findOneBy([
'code' => $newCode,
'bid' => $acc['bid']
]);
// اگر کد تکراری باشد، کد جدید تولید می‌کنیم
if ($existingBankWithCode) {
// تولید کد جدید
$newCode = $provider->getAccountingCode($request->headers->get('activeBid'), 'bank');
// بررسی مجدد
$existingBankWithCode = $entityManager->getRepository(BankAccount::class)->findOneBy([
'code' => $newCode,
'bid' => $acc['bid']
]);
// اگر هنوز تکراری باشد، خطا برگردان
if ($existingBankWithCode) {
return $this->json(['result' => 4, 'message' => 'خطا در تولید کد یکتا برای حساب بانکی']);
}
}
$data = new BankAccount();
$data->setCode($provider->getAccountingCode($request->headers->get('activeBid'), 'bank'));
$data->setCode($newCode);
$data->setMoney($acc['money']);
} else {
$data = $entityManager->getRepository(BankAccount::class)->findOneBy([
@ -159,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'));
@ -184,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()) {
@ -217,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([
@ -257,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'])
@ -299,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) {
@ -315,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 = [
@ -369,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) {
@ -385,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());
@ -545,8 +582,14 @@ class BusinessController extends AbstractController
'plugHrmDocs' => true,
'plugGhestaManager' => true,
'plugTaxSettings' => true,
'plugWarranty' => true,
'plugImportWorkflow' => true,
'inquiry' => true,
'ai' => true,
'warehouseManager' => true,
'importWorkflow' => true,
'plugHrmAttendance' => true,
'storehelper' => true,
];
} elseif ($perm) {
$result = [
@ -591,8 +634,14 @@ class BusinessController extends AbstractController
'plugHrmDocs' => $perm->isPlugHrmDocs(),
'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);
@ -662,9 +711,15 @@ class BusinessController extends AbstractController
$perm->setPlugRepservice($params['plugRepservice']);
$perm->setPlugHrmDocs($params['plugHrmDocs']);
$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);
@ -704,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([
@ -718,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;
@ -741,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;
@ -763,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;
@ -785,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

@ -16,6 +16,8 @@ use App\Entity\Person;
use App\Entity\PrintOptions;
use App\Entity\StoreroomTicket;
use App\Service\Printers;
use App\Entity\CustomInvoiceTemplate;
use App\Service\CustomInvoice\TemplateRenderer;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@ -250,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'],
@ -284,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')]
@ -423,8 +528,10 @@ 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): JsonResponse
public function app_buy_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, TemplateRenderer $renderer): JsonResponse
{
$params = [];
if ($content = $request->getContent()) {
@ -456,53 +563,104 @@ class BuyController extends AbstractController
}
$pdfPid = 0;
if ($params['pdf']) {
$printOptions = [
'bidInfo' => true,
'pays' => true,
'taxInfo' => true,
'discountInfo' => true,
'note' => true,
'paper' => 'A4-L'
];
if (array_key_exists('printOptions', $params)) {
if (array_key_exists('bidInfo', $params['printOptions'])) {
$printOptions['bidInfo'] = $params['printOptions']['bidInfo'];
}
if (array_key_exists('pays', $params['printOptions'])) {
$printOptions['pays'] = $params['printOptions']['pays'];
}
if (array_key_exists('taxInfo', $params['printOptions'])) {
$printOptions['taxInfo'] = $params['printOptions']['taxInfo'];
}
if (array_key_exists('discountInfo', $params['printOptions'])) {
$printOptions['discountInfo'] = $params['printOptions']['discountInfo'];
}
if (array_key_exists('note', $params['printOptions'])) {
$printOptions['note'] = $params['printOptions']['note'];
}
if (array_key_exists('paper', $params['printOptions'])) {
$printOptions['paper'] = $params['printOptions']['paper'];
}
}
$note = '';
// Build print options from defaults and overrides
$printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid' => $acc['bid']]);
$defaultOptions = [
'bidInfo' => $printSettings ? $printSettings->isBuyBidInfo() : true,
'pays' => $printSettings ? $printSettings->isBuyPays() : true,
'taxInfo' => $printSettings ? $printSettings->isBuyTaxInfo() : true,
'discountInfo' => $printSettings ? $printSettings->isBuyDiscountInfo() : true,
'note' => $printSettings ? $printSettings->isBuyNote() : true,
'paper' => $printSettings ? $printSettings->getBuyPaper() : 'A4-L',
];
$printOptions = array_merge($defaultOptions, $params['printOptions'] ?? []);
$note = '';
if ($printSettings) {
$note = $printSettings->getBuyNoteString();
}
$pdfPid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$this->renderView('pdf/printers/buy.html.twig', [
// Build safe context
$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(),
];
}, $doc->getHesabdariRows()->toArray());
$personArr = $person ? [
'name' => $person->getName(),
'mobile' => $person->getMobile(),
'tel' => $person->getTel(),
'address' => $person->getAddress(),
] : null;
$biz = $acc['bid'];
$businessArr = $biz ? [
'name' => method_exists($biz, 'getName') ? $biz->getName() : null,
'tel' => method_exists($biz, 'getTel') ? $biz->getTel() : null,
'mobile' => method_exists($biz, 'getMobile') ? $biz->getMobile() : null,
'address' => method_exists($biz, 'getAddress') ? $biz->getAddress() : null,
'shenasemeli' => method_exists($biz, 'getShenasemeli') ? $biz->getShenasemeli() : null,
'codeeghtesadi' => method_exists($biz, 'getCodeeghtesadi') ? $biz->getCodeeghtesadi() : null,
] : null;
$context = [
'business' => $businessArr,
'doc' => [
'code' => $doc->getCode(),
'date' => method_exists($doc, 'getDate') ? $doc->getDate() : null,
],
'rows' => $rowsArr,
'person' => $personArr,
'discount' => $discount,
'transfer' => $transfer,
'printOptions' => $printOptions,
'note' => $note,
];
// Decide template: custom or default
$html = null;
$selectedTemplate = $printSettings ? $printSettings->getBuyTemplate() : null;
if ($selectedTemplate instanceof CustomInvoiceTemplate) {
$html = $renderer->render($selectedTemplate->getCode() ?? '', $context);
}
if ($html === null) {
$html = $this->renderView('pdf/printers/buy.html.twig', [
'bid' => $acc['bid'],
'doc' => $doc,
'rows' => $doc->getHesabdariRows(),
'rows' => array_map(function ($row) {
return [
'commodity' => $row->getCommodity(),
'commodityCount' => $row->getCommdityCount(),
'commdityCount' => $row->getCommdityCount(),
'des' => $row->getDes(),
'bs' => $row->getBs(),
'bd' => $row->getBd(),
'tax' => $row->getTax(),
'discount' => $row->getDiscount(),
];
}, $doc->getHesabdariRows()->toArray()),
'person' => $person,
'printInvoice' => $params['printers'],
'discount' => $discount,
'transfer' => $transfer,
'printOptions' => $printOptions,
'note' => $note
]),
]);
}
$pdfPid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$html,
false,
$printOptions['paper']
);
@ -514,7 +672,18 @@ class BuyController extends AbstractController
$this->renderView('pdf/posPrinters/justBuy.html.twig', [
'bid' => $acc['bid'],
'doc' => $doc,
'rows' => $doc->getHesabdariRows(),
'rows' => array_map(function ($row) {
return [
'commodity' => $row->getCommodity(),
'commodityCount' => $row->getCommdityCount(),
'commdityCount' => $row->getCommdityCount(),
'des' => $row->getDes(),
'bs' => $row->getBs(),
'bd' => $row->getBd(),
'tax' => $row->getTax(),
'discount' => $row->getDiscount(),
];
}, $doc->getHesabdariRows()->toArray()),
]),
false
);
@ -522,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

@ -0,0 +1,672 @@
<?php
namespace App\Controller;
use App\Entity\ChatChannel;
use App\Entity\ChatMessage;
use App\Entity\User;
use App\Repository\ChatChannelRepository;
use App\Repository\ChatMessageRepository;
use App\Repository\UserRepository;
use App\Service\ChatService;
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\Bundle\SecurityBundle\Security;
#[Route('/api/chat')]
class ChatController extends AbstractController
{
public function __construct(
private ChatService $chatService,
private EntityManagerInterface $entityManager,
private ChatChannelRepository $channelRepository,
private ChatMessageRepository $messageRepository,
private UserRepository $userRepository,
private Security $security
) {}
#[Route('/channels', name: 'chat_channels', methods: ['GET'])]
public function getUserChannels(): JsonResponse
{
/** @var User $user */
$user = $this->security->getUser();
if (!$user) {
return $this->json([
'success' => false,
'message' => 'کاربر احراز هویت نشده است'
], Response::HTTP_UNAUTHORIZED);
}
$channels = $this->chatService->getUserChannels($user);
$data = [];
foreach ($channels as $channel) {
$data[] = [
'id' => $channel->getId(),
'channelId' => $channel->getChannelId(),
'name' => $channel->getName(),
'description' => $channel->getDescription(),
'isPublic' => $channel->isPublic(),
'avatar' => $channel->getAvatar(),
'messageCount' => $channel->getMessageCount(),
'memberCount' => $channel->getMemberCount(),
'lastMessageAt' => $channel->getLastMessageAt()?->format('Y-m-d H:i:s'),
'createdAt' => $channel->getCreatedAt()->format('Y-m-d H:i:s'),
'isAdmin' => $this->chatService->isUserAdmin($channel, $user),
];
}
return $this->json([
'success' => true,
'data' => $data
]);
}
#[Route('/channels', name: 'chat_create_channel', methods: ['POST'])]
public function createChannel(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
if (!isset($data['name']) || empty($data['name'])) {
return $this->json([
'success' => false,
'message' => 'نام کانال الزامی است'
], Response::HTTP_BAD_REQUEST);
}
/** @var User $user */
$user = $this->security->getUser();
try {
$channel = $this->chatService->createChannel(
$data['name'],
$data['description'] ?? '',
$data['isPublic'] ?? true,
$user
);
return $this->json([
'success' => true,
'data' => [
'id' => $channel->getId(),
'channelId' => $channel->getChannelId(),
'name' => $channel->getName(),
'description' => $channel->getDescription(),
'isPublic' => $channel->isPublic(),
]
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/channels/search', name: 'chat_search_channels', methods: ['GET'])]
public function searchChannels(Request $request): JsonResponse
{
$searchTerm = $request->query->get('q', '');
if (empty($searchTerm)) {
// Return popular public channels when search term is empty
$channels = $this->chatService->getPopularPublicChannels(10);
} else {
$channels = $this->chatService->searchPublicChannels($searchTerm);
}
$data = [];
foreach ($channels as $channel) {
$data[] = [
'id' => $channel->getId(),
'channelId' => $channel->getChannelId(),
'name' => $channel->getName(),
'description' => $channel->getDescription(),
'isPublic' => $channel->isPublic(),
'messageCount' => $channel->getMessageCount(),
'memberCount' => $channel->getMemberCount(),
'lastMessageAt' => $channel->getLastMessageAt()?->format('Y-m-d H:i:s'),
];
}
return $this->json([
'success' => true,
'data' => $data
]);
}
#[Route('/channels/{channelId}/join', name: 'chat_join_channel', methods: ['POST'])]
public function joinChannel(string $channelId): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
/** @var User $user */
$user = $this->security->getUser();
try {
$success = $this->chatService->joinChannel($channel, $user);
if ($success) {
return $this->json([
'success' => true,
'message' => 'با موفقیت به کانال پیوستید'
]);
} else {
return $this->json([
'success' => false,
'message' => 'قبلاً عضو این کانال هستید'
], Response::HTTP_BAD_REQUEST);
}
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/channels/{channelId}/members', name: 'chat_add_member', methods: ['POST'])]
public function addMember(string $channelId, Request $request): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
$data = json_decode($request->getContent(), true);
if (!isset($data['userId']) || empty($data['userId'])) {
return $this->json([
'success' => false,
'message' => 'شناسه کاربر الزامی است'
], Response::HTTP_BAD_REQUEST);
}
/** @var User $admin */
$admin = $this->security->getUser();
// Check if admin is actually an admin of this channel
if (!$this->chatService->isUserAdmin($channel, $admin)) {
return $this->json([
'success' => false,
'message' => 'شما دسترسی لازم برای اضافه کردن عضو ندارید'
], Response::HTTP_FORBIDDEN);
}
$user = $this->userRepository->find($data['userId']);
if (!$user) {
return $this->json([
'success' => false,
'message' => 'کاربر یافت نشد'
], Response::HTTP_NOT_FOUND);
}
try {
$success = $this->chatService->addMemberToChannel($channel, $user, $admin);
if ($success) {
return $this->json([
'success' => true,
'message' => 'عضو با موفقیت به کانال اضافه شد'
]);
} else {
return $this->json([
'success' => false,
'message' => 'کاربر قبلاً عضو این کانال است'
], Response::HTTP_BAD_REQUEST);
}
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/channels/{channelId}/members/{userId}', name: 'chat_remove_member', methods: ['DELETE'])]
public function removeMember(string $channelId, int $userId): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
/** @var User $admin */
$admin = $this->security->getUser();
// Check if admin is actually an admin of this channel
if (!$this->chatService->isUserAdmin($channel, $admin)) {
return $this->json([
'success' => false,
'message' => 'شما دسترسی لازم برای حذف عضو ندارید'
], Response::HTTP_FORBIDDEN);
}
$user = $this->userRepository->find($userId);
if (!$user) {
return $this->json([
'success' => false,
'message' => 'کاربر یافت نشد'
], Response::HTTP_NOT_FOUND);
}
try {
$success = $this->chatService->removeMemberFromChannel($channel, $user, $admin);
if ($success) {
return $this->json([
'success' => true,
'message' => 'عضو با موفقیت از کانال حذف شد'
]);
} else {
return $this->json([
'success' => false,
'message' => 'کاربر عضو این کانال نیست'
], Response::HTTP_BAD_REQUEST);
}
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/channels/{channelId}/members', name: 'chat_get_members', methods: ['GET'])]
public function getChannelMembers(string $channelId): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
/** @var User $user */
$user = $this->security->getUser();
// Check if user is member
if (!$this->chatService->isUserMember($channel, $user)) {
return $this->json([
'success' => false,
'message' => 'شما عضو این کانال نیستید'
], Response::HTTP_FORBIDDEN);
}
$members = $this->chatService->getChannelMembers($channel);
$data = [];
foreach ($members as $member) {
$data[] = [
'id' => $member->getUser()->getId(),
'fullName' => $member->getUser()->getFullName(),
'email' => $member->getUser()->getEmail(),
'isAdmin' => $member->isAdmin(),
'joinedAt' => $member->getJoinedAt()->format('Y-m-d H:i:s'),
];
}
return $this->json([
'success' => true,
'data' => $data
]);
}
#[Route('/channels/{channelId}/leave', name: 'chat_leave_channel', methods: ['POST'])]
public function leaveChannel(string $channelId): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
/** @var User $user */
$user = $this->security->getUser();
try {
$success = $this->chatService->leaveChannel($channel, $user);
if ($success) {
return $this->json([
'success' => true,
'message' => 'با موفقیت از کانال خارج شدید'
]);
} else {
return $this->json([
'success' => false,
'message' => 'شما عضو این کانال نیستید'
], Response::HTTP_BAD_REQUEST);
}
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/channels/{channelId}/messages', name: 'chat_channel_messages', methods: ['GET'])]
public function getChannelMessages(string $channelId, Request $request): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
/** @var User $user */
$user = $this->security->getUser();
// For private channels, check if user is member
if (!$channel->isPublic() && !$this->chatService->isUserMember($channel, $user)) {
return $this->json([
'success' => false,
'message' => 'شما عضو این کانال نیستید'
], Response::HTTP_FORBIDDEN);
}
$limit = (int) $request->query->get('limit', 30);
$offset = (int) $request->query->get('offset', 0);
$messages = $this->chatService->getChannelMessages($channel, $limit, $offset);
// Get total message count for pagination info
$totalMessages = $this->chatService->getChannelMessageCount($channel);
$data = [];
foreach ($messages as $message) {
$data[] = [
'id' => $message->getId(),
'content' => $message->getContent(),
'messageType' => $message->getMessageType(),
'sentAt' => $message->getSentAt()->format('Y-m-d H:i:s'),
'isEdited' => $message->isEdited(),
'editedAt' => $message->getEditedAt()?->format('Y-m-d H:i:s'),
'sender' => [
'id' => $message->getSender()->getId(),
'fullName' => $message->getSender()->getFullName(),
'email' => $message->getSender()->getEmail(),
],
'quotedMessage' => $message->getQuotedMessage() ? [
'id' => $message->getQuotedMessage()->getId(),
'content' => $message->getQuotedMessage()->getContent(),
'sender' => $message->getQuotedMessage()->getSender()->getFullName(),
] : null,
'reactions' => $message->getReactions() ?: [],
'attachments' => $message->getAttachments(),
];
}
return $this->json([
'success' => true,
'data' => $data,
'pagination' => [
'limit' => $limit,
'offset' => $offset,
'total' => $totalMessages,
'hasMore' => ($offset + $limit) < $totalMessages
]
]);
}
#[Route('/channels/{channelId}/messages', name: 'chat_send_message', methods: ['POST'])]
public function sendMessage(string $channelId, Request $request): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
$data = json_decode($request->getContent(), true);
if (!isset($data['content']) || empty($data['content'])) {
return $this->json([
'success' => false,
'message' => 'متن پیام الزامی است'
], Response::HTTP_BAD_REQUEST);
}
/** @var User $user */
$user = $this->security->getUser();
// Check if user is member (required for sending messages)
if (!$this->chatService->isUserMember($channel, $user)) {
return $this->json([
'success' => false,
'message' => 'برای ارسال پیام باید عضو کانال باشید'
], Response::HTTP_FORBIDDEN);
}
try {
$quotedMessage = null;
if (isset($data['quotedMessageId'])) {
$quotedMessage = $this->messageRepository->find($data['quotedMessageId']);
}
$message = $this->chatService->sendMessage(
$channel,
$user,
$data['content'],
$data['messageType'] ?? 'text',
$quotedMessage
);
return $this->json([
'success' => true,
'data' => [
'id' => $message->getId(),
'content' => $message->getContent(),
'messageType' => $message->getMessageType(),
'sentAt' => $message->getSentAt()->format('Y-m-d H:i:s'),
'sender' => [
'id' => $message->getSender()->getId(),
'fullName' => $message->getSender()->getFullName(),
],
'quotedMessage' => $message->getQuotedMessage() ? [
'id' => $message->getQuotedMessage()->getId(),
'content' => $message->getQuotedMessage()->getContent(),
'sender' => $message->getQuotedMessage()->getSender()->getFullName(),
] : null,
]
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/messages/{messageId}/edit', name: 'chat_edit_message', methods: ['PUT'])]
public function editMessage(int $messageId, Request $request): JsonResponse
{
$message = $this->messageRepository->find($messageId);
if (!$message) {
return $this->json([
'success' => false,
'message' => 'پیام یافت نشد'
], Response::HTTP_NOT_FOUND);
}
$data = json_decode($request->getContent(), true);
if (!isset($data['content']) || empty($data['content'])) {
return $this->json([
'success' => false,
'message' => 'متن پیام الزامی است'
], Response::HTTP_BAD_REQUEST);
}
/** @var User $user */
$user = $this->security->getUser();
$success = $this->chatService->editMessage($message, $user, $data['content']);
if ($success) {
return $this->json([
'success' => true,
'message' => 'پیام با موفقیت ویرایش شد'
]);
} else {
return $this->json([
'success' => false,
'message' => 'شما نمی‌توانید این پیام را ویرایش کنید'
], Response::HTTP_FORBIDDEN);
}
}
#[Route('/messages/{messageId}/reactions', name: 'chat_add_reaction', methods: ['POST'])]
public function addReaction(int $messageId, Request $request): JsonResponse
{
$message = $this->messageRepository->find($messageId);
if (!$message) {
return $this->json([
'success' => false,
'message' => 'پیام یافت نشد'
], Response::HTTP_NOT_FOUND);
}
$data = json_decode($request->getContent(), true);
if (!isset($data['emoji']) || empty($data['emoji'])) {
return $this->json([
'success' => false,
'message' => 'ایموجی الزامی است'
], Response::HTTP_BAD_REQUEST);
}
/** @var User $user */
$user = $this->security->getUser();
$success = $this->chatService->addReaction($message, $user, $data['emoji']);
if ($success) {
return $this->json([
'success' => true,
'message' => 'واکنش اضافه شد'
]);
} else {
return $this->json([
'success' => false,
'message' => 'خطا در اضافه کردن واکنش'
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/messages/{messageId}/reactions', name: 'chat_remove_reaction', methods: ['DELETE'])]
public function removeReaction(int $messageId, Request $request): JsonResponse
{
$message = $this->messageRepository->find($messageId);
if (!$message) {
return $this->json([
'success' => false,
'message' => 'پیام یافت نشد'
], Response::HTTP_NOT_FOUND);
}
$data = json_decode($request->getContent(), true);
if (!isset($data['emoji']) || empty($data['emoji'])) {
return $this->json([
'success' => false,
'message' => 'ایموجی الزامی است'
], Response::HTTP_BAD_REQUEST);
}
/** @var User $user */
$user = $this->security->getUser();
$success = $this->chatService->removeReaction($message, $user, $data['emoji']);
if ($success) {
return $this->json([
'success' => true,
'message' => 'واکنش حذف شد'
]);
} else {
return $this->json([
'success' => false,
'message' => 'خطا در حذف واکنش'
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/users/search', name: 'chat_search_users', methods: ['GET'])]
public function searchUsers(Request $request): JsonResponse
{
$searchTerm = $request->query->get('q', '');
if (empty($searchTerm)) {
return $this->json([
'success' => false,
'message' => 'عبارت جستجو الزامی است'
], Response::HTTP_BAD_REQUEST);
}
$users = $this->chatService->searchUsers($searchTerm);
$data = [];
foreach ($users as $user) {
$data[] = [
'id' => $user->getId(),
'fullName' => $user->getFullName(),
'email' => $user->getEmail(),
];
}
return $this->json([
'success' => true,
'data' => $data
]);
}
#[Route('/channels/{channelId}/stats', name: 'chat_channel_stats', methods: ['GET'])]
public function getChannelStats(string $channelId): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
$stats = $this->chatService->getChannelStats($channel);
$messageStats = $this->chatService->getMessageStats($channel);
return $this->json([
'success' => true,
'data' => [
'memberCount' => $stats['memberCount'],
'messageCount' => $stats['messageCount'],
'messageStats' => $messageStats,
]
]);
}
}

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') {
@ -182,7 +191,32 @@ class CommodityController extends AbstractController
]);
$res = [];
foreach ($items as $item) {
$res[] = Explore::ExploreCommodity($item);
$temp = Explore::ExploreCommodity($item);
if (!$item->isKhadamat()) {
// 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') {
$count += $row->getCommdityCount();
} else {
$count -= $row->getCommdityCount();
}
}
$temp['count'] = $count;
}
$res[] = $temp;
}
return $this->json($extractor->operationSuccess([
'List' => $res,
@ -267,14 +301,24 @@ class CommodityController extends AbstractController
$temp['taxCode'] = $item->getTaxCode();
$temp['taxType'] = $item->getTaxType();
$temp['taxUnit'] = $item->getTaxUnit();
$temp['customCode'] = $item->isCustomCode();
//calculate count
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') {
@ -334,14 +378,24 @@ class CommodityController extends AbstractController
$temp['taxCode'] = $item->getTaxCode();
$temp['taxType'] = $item->getTaxType();
$temp['taxUnit'] = $item->getTaxUnit();
$temp['customCode'] = $item->isCustomCode();
//calculate count
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') {
@ -429,14 +483,24 @@ class CommodityController extends AbstractController
$temp['taxCode'] = $item->getTaxCode();
$temp['taxType'] = $item->getTaxType();
$temp['taxUnit'] = $item->getTaxUnit();
$temp['customCode'] = $item->isCustomCode();
//calculate count
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') {
@ -528,7 +592,8 @@ class CommodityController extends AbstractController
$temp[] = $item->getMinOrderCount();
$temp[] = $item->getDes();
$temp[] = $item->getUnit()->getName();
$temp[] = $item->getCat()->getName();
$cat = $item->getCat();
$temp[] = $cat ? $cat->getName() : '';
$array[] = $temp;
}
$filePath = $provider->createExcellFromArray($array, [
@ -604,29 +669,46 @@ class CommodityController extends AbstractController
return $this->json(['id' => $pid]);
}
#[Route('/api/commodity/info/{code}', name: 'app_commodity_info')]
public function app_commodity_info($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
#[Route('/api/commodity/info/{id}', name: 'app_commodity_info')]
public function app_commodity_info($id, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('commodity');
if (!$acc)
throw $this->createAccessDeniedException();
$data = $entityManager->getRepository(Commodity::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
'code' => $id
]);
if (!$data) {
return $this->json(['error' => 'کالا یافت نشد'], 404);
}
$res = Explore::ExploreCommodity($data);
$res['cat'] = '';
if ($data->getCat())
$res['cat'] = $data->getCat()->getId();
$cat = $data->getCat();
if ($cat !== null) {
$res['cat'] = $cat->getId();
}
$res['customCode'] = $data->isCustomCode();
$count = 0;
//calculate count
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();
@ -663,7 +745,7 @@ class CommodityController extends AbstractController
}
}
$res['Success'] = true;
return $this->json($res);
}
@ -679,11 +761,14 @@ class CommodityController extends AbstractController
}
if (!array_key_exists('items', $paramsAll))
return $this->json($extractor->paramsNotSend());
$results = [];
$createdItems = [];
foreach ($paramsAll['items'] as $params) {
if (!array_key_exists('name', $params))
return $this->json(['result' => -1]);
if (count_chars(trim($params['name'])) == 0)
return $this->json(['result' => 3]);
$isNew = false;
if ($code == 0) {
$data = $entityManager->getRepository(Commodity::class)->findOneBy([
'name' => $params['name'],
@ -693,6 +778,7 @@ class CommodityController extends AbstractController
if (!$data) {
$data = new Commodity();
$data->setCode($provider->getAccountingCode($request->headers->get('activeBid'), 'Commodity'));
$isNew = true;
}
} else {
$data = $entityManager->getRepository(Commodity::class)->findOneBy([
@ -779,6 +865,13 @@ class CommodityController extends AbstractController
}
}
}
if (array_key_exists('Tag', $params)) {
$tagValue = $params['Tag'];
if (is_string($tagValue)) {
$tagValue = json_decode($tagValue, true);
}
$data->setTags($tagValue);
}
$entityManager->persist($data);
//save prices list
@ -807,14 +900,45 @@ class CommodityController extends AbstractController
}
$entityManager->flush();
$log->insert('کالا و خدمات', 'کالا / خدمات با نام ' . $params['name'] . ' افزوده/ویرایش شد.', $this->getUser(), $request->headers->get('activeBid'));
$createdItems[] = [
'id' => $data->getId(),
'name' => $data->getName(),
'code' => $data->getCode(),
'unit' => $data->getUnit() ? $data->getUnit()->getName() : null,
'khadamat' => $data->isKhadamat(),
'withoutTax' => $data->isWithoutTax(),
'des' => $data->getDes(),
'priceSell' => $data->getPriceSell(),
'priceBuy' => $data->getPriceBuy(),
'commodityCountCheck' => $data->isCommodityCountCheck(),
'barcodes' => $data->getBarcodes(),
'taxCode' => $data->getTaxCode(),
'taxType' => $data->getTaxType(),
'taxUnit' => $data->getTaxUnit(),
'minOrderCount' => $data->getMinOrderCount(),
'speedAccess' => $data->isSpeedAccess(),
'dayLoading' => $data->getDayLoading(),
'orderPoint' => $data->getOrderPoint(),
'cat' => $data->getCat() ? $data->getCat()->getId() : null,
'tags' => $data->getTags(),
];
}
if (isset($paramsAll['reqType']) && $paramsAll['reqType'] === 'woocommercePlugin') {
return $this->json([
'Success' => true,
'Success' => 1,
'result' => 1,
'createdItems' => $createdItems
]);
} else {
return $this->json([
'Success' => 1,
'result' => 1
]);
}
#[Route('/api/commodity/mod/{code}', name: 'app_commodity_mod')]
public function app_commodity_mod(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $code = 0): JsonResponse
}
#[Route('/api/commodity/mod/{id}', name: 'app_commodity_mod')]
public function app_commodity_mod(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $id = 0): JsonResponse
{
$acc = $access->hasRole('commodity');
if (!$acc)
@ -823,138 +947,13 @@ class CommodityController extends AbstractController
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('name', $params))
return $this->json(['result' => -1]);
if (count_chars(trim($params['name'])) == 0)
return $this->json(['result' => 3]);
if ($code == 0) {
$data = $entityManager->getRepository(Commodity::class)->findOneBy([
'name' => $params['name'],
'bid' => $acc['bid']
]);
//check exist before
if (!$data) {
$data = new Commodity();
$data->setCode($provider->getAccountingCode($request->headers->get('activeBid'), 'Commodity'));
$commodityService = new \App\Cog\CommodityService($entityManager);
$result = $commodityService->addOrUpdateCommodity($params, $acc, $id);
if (isset($result['error'])) {
return $this->json($result, 400);
}
} else {
$data = $entityManager->getRepository(Commodity::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
if (!$data)
throw $this->createNotFoundException();
}
if (!array_key_exists('unit', $params))
$unit = $entityManager->getRepository(CommodityUnit::class)->findAll()[0];
else
$unit = $entityManager->getRepository(CommodityUnit::class)->findOneBy(['name' => $params['unit']]);
if (!$unit)
throw $this->createNotFoundException('unit not fount!');
$data->setUnit($unit);
$data->setBid($acc['bid']);
$data->setname($params['name']);
if ($params['khadamat'] == 'true')
$data->setKhadamat(true);
else
$data->setKhadamat(false);
if (!array_key_exists('withoutTax', $params))
$data->setWithoutTax(false);
else {
if ($params['withoutTax'] == 'true')
$data->setWithoutTax(true);
else
$data->setWithoutTax(false);
}
if (array_key_exists('des', $params))
$data->setDes($params['des']);
if (array_key_exists('priceSell', $params))
$data->setPriceSell($params['priceSell']);
if (array_key_exists('priceBuy', $params))
$data->setPriceBuy($params['priceBuy']);
if (array_key_exists('commodityCountCheck', $params)) {
$data->setCommodityCountCheck($params['commodityCountCheck']);
}
if (array_key_exists('barcodes', $params)) {
$data->setBarcodes($params['barcodes']);
}
if (array_key_exists('taxCode', $params)) {
$data->setTaxCode($params['taxCode']);
}
if (array_key_exists('taxType', $params)) {
$data->setTaxType($params['taxType']);
}
if (array_key_exists('taxUnit', $params)) {
$data->setTaxUnit($params['taxUnit']);
}
if (array_key_exists('minOrderCount', $params)) {
$data->setMinOrderCount($params['minOrderCount']);
}
if (array_key_exists('speedAccess', $params)) {
$data->setSpeedAccess($params['speedAccess']);
}
if (array_key_exists('dayLoading', $params)) {
$data->setDayLoading($params['dayLoading']);
}
if (array_key_exists('orderPoint', $params)) {
$data->setOrderPoint($params['orderPoint']);
}
//set cat
if (array_key_exists('cat', $params)) {
if ($params['cat'] != '') {
if (is_int($params['cat']))
$cat = $entityManager->getRepository(CommodityCat::class)->find($params['cat']);
else
$cat = $entityManager->getRepository(CommodityCat::class)->find($params['cat']['id']);
if ($cat) {
if ($cat->getBid() == $acc['bid']) {
$data->setCat($cat);
}
}
}
}
$entityManager->persist($data);
//save prices list
if (array_key_exists('prices', $params)) {
foreach ($params['prices'] as $item) {
$priceList = $entityManager->getRepository(PriceList::class)->findOneBy([
'bid' => $acc['bid'],
'id' => $item['list']['id']
]);
if ($priceList) {
$detail = $entityManager->getRepository(PriceListDetail::class)->findOneBy([
'list' => $priceList,
'commodity' => $data
]);
if (!$detail) {
$detail = new PriceListDetail;
}
$detail->setList($priceList);
$detail->setCommodity($data);
$detail->setPriceSell($item['priceSell']);
$detail->setPriceBuy(0);
$detail->setMoney($acc['money']);
$entityManager->persist($detail);
}
}
}
$entityManager->flush();
$log->insert('کالا و خدمات', 'کالا / خدمات با نام ' . $params['name'] . ' افزوده/ویرایش شد.', $this->getUser(), $request->headers->get('activeBid'));
return $this->json([
'Success' => true,
'result' => 1,
'code' => $data->getId()
]);
return $this->json($result);
}
#[Route('/api/commodity/units', name: 'app_commodity_units')]
@ -1275,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) {
@ -1375,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' => 'این کالا در اسناد حسابداری استفاده شده و قابل حذف نیست']);
}
@ -1426,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) {
@ -281,7 +295,7 @@ class DirectHesabdariDoc extends AbstractController
}
#[Route('/api/hesabdari/direct/doc/delete/{id}', name: 'delete_hesabdari_doc_delete')]
public function delete(Log $log, Access $access, int $id, EntityManagerInterface $entityManager): JsonResponse
public function delete(\App\Service\Log $logservice, Access $access, int $id, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('accounting');
if (!$acc) {
@ -299,9 +313,14 @@ class DirectHesabdariDoc extends AbstractController
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به سند'], 403);
}
$logs = $entityManager->getRepository(\App\Entity\Log::class)->findBy(['doc' => $hesabdariDoc]);
foreach ($logs as $log) {
$log->setDoc(null);
$entityManager->persist($log);
}
$entityManager->remove($hesabdariDoc);
$entityManager->flush();
$log->insert('حسابداری', 'حذف سند حسابداری شماره ' . $hesabdariDoc->getCode(), $this->getUser(), $acc['bid'], $hesabdariDoc);
$logservice->insert('حسابداری', 'حذف سند حسابداری شماره ' . $hesabdariDoc->getCode(), $this->getUser(), $acc['bid'], null);
return new JsonResponse(['success' => true, 'message' => 'سند با موفقیت حذف شد'], 200);
}
@ -350,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
@ -188,7 +201,7 @@ class HesabdariController extends AbstractController
HesabdariTableRepository $hesabdariTableRepository,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('acc');
$acc = $access->hasRole('accounting');
if (!$acc) {
throw $this->createAccessDeniedException();
}
@ -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))
@ -848,99 +894,15 @@ class HesabdariController extends AbstractController
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('type', $params))
$this->createNotFoundException();
$roll = '';
if ($params['type'] == 'person')
$roll = 'person';
if ($params['type'] == 'person_receive' || $params['type'] == 'person_send')
$roll = 'person';
elseif ($params['type'] == 'sell_receive')
$roll = 'sell';
elseif ($params['type'] == 'bank')
$roll = 'banks';
elseif ($params['type'] == 'buy_send')
$roll = 'buy';
elseif ($params['type'] == 'transfer')
$roll = 'bankTransfer';
elseif ($params['type'] == 'all')
$roll = 'accounting';
else
$roll = $params['type'];
$acc = $access->hasRole($roll);
$acc = $access->hasRole($params['type'] ?? 'accounting');
if (!$acc)
throw $this->createAccessDeniedException();
if ($params['type'] == 'person') {
$person = $entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$person)
throw $this->createNotFoundException();
$data = $entityManager->getRepository(HesabdariRow::class)->findBy([
'person' => $person,
], [
'id' => 'DESC'
]);
} elseif ($params['type'] == 'bank') {
$bank = $entityManager->getRepository(BankAccount::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$bank)
throw $this->createNotFoundException();
$data = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bank' => $bank,
], [
'id' => 'DESC'
]);
} elseif ($params['type'] == 'cashdesk') {
$cashdesk = $entityManager->getRepository(Cashdesk::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$cashdesk)
throw $this->createNotFoundException();
$data = $entityManager->getRepository(HesabdariRow::class)->findBy([
'cashdesk' => $cashdesk,
], [
'id' => 'DESC'
]);
} elseif ($params['type'] == 'salary') {
$salary = $entityManager->getRepository(Salary::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$salary)
throw $this->createNotFoundException();
$data = $entityManager->getRepository(HesabdariRow::class)->findBy([
'salary' => $salary,
], [
'id' => 'DESC'
]);
$service = new \App\Cog\AccountingDocService($entityManager);
$result = $service->searchRows($params, $acc);
if (isset($result['error'])) {
return $this->json($result, 400);
}
$dataTemp = [];
foreach ($data as $item) {
$temp = [
'id' => $item->getId(),
'dateSubmit' => $item->getDoc()->getDateSubmit(),
'date' => $item->getDoc()->getDate(),
'type' => $item->getDoc()->getType(),
'ref' => $item->getRef()->getName(),
'des' => $item->getDes(),
'bs' => $item->getBs(),
'bd' => $item->getBd(),
'code' => $item->getDoc()->getCode(),
'submitter' => $item->getDoc()->getSubmitter()->getFullName()
];
$dataTemp[] = $temp;
}
return $this->json($dataTemp);
return $this->json($result);
}
#[Route('/api/accounting/table/get', name: 'app_accounting_table_get')]
@ -966,12 +928,14 @@ class HesabdariController extends AbstractController
$temp[$node->getCode()] = [
'text' => $node->getName(),
'id' => $node->getCode() ?? $node->getId(),
'type' => $node->getType(),
'children' => $this->getFilteredChildsLabel($entityManager, $node, $business),
];
} else {
$temp[$node->getCode()] = [
'text' => $node->getName(),
'id' => $node->getCode() ?? $node->getId(),
'type' => $node->getType(),
];
}
$temp[$node->getCode()]['is_public'] = $nodeBid === null;
@ -1117,6 +1081,13 @@ class HesabdariController extends AbstractController
return $this->json(['result' => 0, 'message' => 'نام ردیف حساب و آیدی والد الزامی است'], 400);
}
// بررسی نوع تفضیل حساب
$allowedTypes = ['calc', 'person', 'commodity', 'bank', 'salary', 'cashdesk'];
$accountType = $params['accountType'] ?? 'calc';
if (!in_array($accountType, $allowedTypes)) {
return $this->json(['result' => 0, 'message' => 'نوع تفضیل حساب نامعتبر است'], 400);
}
$parentNode = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => $params['parentId']]);
if (!$parentNode) {
return $this->json(['result' => 0, 'message' => 'ردیف حساب والد پیدا نشد'], 404);
@ -1142,18 +1113,19 @@ class HesabdariController extends AbstractController
$newNode->setCode($uniqueCode);
$newNode->setBid($acc['bid']);
$newNode->setUpper($parentNode);
$newNode->setType('calc');
$newNode->setType($accountType);
$entityManager->persist($newNode);
$entityManager->flush();
$log->insert('حسابداری', 'ردیف حساب جدید با کد ' . $newNode->getCode() . ' اضافه شد.', $this->getUser(), $acc['bid']);
$log->insert('حسابداری', 'ردیف حساب جدید با کد ' . $newNode->getCode() . ' و نوع ' . $accountType . ' اضافه شد.', $this->getUser(), $acc['bid']);
return $this->json([
'result' => 1,
'node' => [
'id' => $newNode->getCode(),
'text' => $newNode->getName(),
'type' => $newNode->getType(),
'children' => [],
'is_public' => $newNode->getBid() ? false : true,
]
@ -1194,6 +1166,7 @@ class HesabdariController extends AbstractController
'node' => [
'id' => $node->getCode(),
'text' => $node->getName(),
'type' => $node->getType(),
'children' => $this->getChildsLabel($entityManager, $node),
'is_public' => $node->getBid() ? false : true,
]

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

@ -20,7 +20,7 @@ use App\Entity\Permission;
use App\Entity\BankAccount;
use App\Entity\CommodityCat;
use App\Entity\HesabdariDoc;
use App\Cog\PersonService;
use App\Entity\HesabdariRow;
use App\Entity\CommodityUnit;
use Doctrine\ORM\EntityManagerInterface;
@ -31,6 +31,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class HookController extends AbstractController
@ -71,6 +72,68 @@ class HookController extends AbstractController
]);
}
#[Route('/hooks/modify/person', name: 'hook_modify_person')]
public function hook_modify_person(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $code = 0): JsonResponse
{
$acc = $access->hasRole('person');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$personService = new \App\Cog\PersonService($entityManager);
$result = $personService->addOrUpdatePerson($params, $acc, $code);
if (isset($result['error'])) {
return $this->json($result, 400);
}
$log->insert('اشخاص', 'شخص با نام مستعار ' . $params['nikename'] . ' افزوده/ویرایش شد.', $this->getUser(), $acc['bid']);
$person = $personService->getPersonInfo($result['code'], $acc);
$result['person'] = $person;
return $this->json($result);
}
#[Route('/hooks/modify/commodity', name: 'app_modify_commodity')]
public function app_modify_commodity(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $id = 0): JsonResponse
{
$acc = $access->hasRole('commodity');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$commodityService = new \App\Cog\CommodityService($entityManager);
$result = $commodityService->addOrUpdateCommodity($params, $acc, $id);
if (isset($result['error'])) {
return $this->json($result, 400);
}
$log->insert('کالا و خدمات', 'کالا / خدمات با نام ' . $params['name'] . ' افزوده/ویرایش شد.', $this->getUser(), $request->headers->get('activeBid'));
return $this->json($result);
}
#[Route('/hooks/info/person', name: 'hook_info_person')]
public function hook_info_person($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, PersonService $personService): JsonResponse
{
$acc = $access->hasRole('person');
if (!$acc)
throw $this->createAccessDeniedException();
$response = $personService->getPersonInfo($code, $acc);
$response['Success'] = true;
return $this->json($response);
}
#[Route('hooks/setting/getCurrency', name: 'api_hooks_getcurrency')]
public function api_hooks_getcurrency(Access $access, Log $log, Request $request, EntityManagerInterface $entityManager): JsonResponse
{
@ -342,4 +405,71 @@ class HookController extends AbstractController
'Result' => $response
]);
}
#[Route('api/wordpress/plugin/stats', name: 'api_wordpress_plugin_stats', methods: ['GET'])]
public function api_wordpress_plugin_stats(HttpClientInterface $httpClient): JsonResponse
{
$apiUrl = 'https://source.hesabix.ir/api/v1/repos/morrning/hesabixWCPlugin/releases';
try {
$response = $httpClient->request('GET', $apiUrl);
$releases = $response->toArray();
if (empty($releases)) {
return $this->json([
'Success' => false,
'ErrorCode' => 1,
'ErrorMessage' => 'No releases found.',
'Result' => null,
]);
}
$latest = $releases[0];
$version = $latest['tag_name'];
$description = $latest['body'];
$lastUpdate = (new \DateTime($latest['published_at']))->format('Y-m-d');
$downloadUrl = $latest['assets'][0]['browser_download_url'] ?? null;
$downloadCount = $latest['assets'][0]['download_count'] ?? 0;
return $this->json([
'Success' => true,
'ErrorCode' => 0,
'ErrorMessage' => '',
'Result' => [
'version' => $version,
'plugin_name' => 'Hesabix: WooCommerce',
'description' => 'پلاگین حسابیکس برای وردپرس',
'author' => 'Mohammad Rzai',
'author_url' => 'https://pirouz.xyz',
'last_update' => $lastUpdate,
'compatibility' => [
'wordpress' => '5.0+',
'php' => '7.4+'
],
'download_url' => $downloadUrl,
'changelog' => [
$version => [
'date' => $lastUpdate,
'changes' => preg_split('/\r\n|\n|\r/', $description),
]
],
// 'statistics' => [
// 'total_installations' => 1250,
// 'active_installations' => 1180,
// 'total_downloads' => 3500 + $downloadCount,
// 'average_rating' => 4.8,
// 'support_tickets' => 45
// ]
]
]);
} catch (\Exception $e) {
return $this->json([
'Success' => false,
'ErrorCode' => 2,
'ErrorMessage' => 'Failed to fetch plugin release data.',
'Result' => null,
]);
}
}
}

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);
@ -452,6 +470,15 @@ class PluginController extends AbstractController
'icon' => 'repservice.jpg',
'defaultOn' => null,
],
[
'name' => 'افزونه طراحی فاکتور اختصاصی',
'code' => 'custominvoice',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '200000',
'icon' => 'custominvoice.png',
'defaultOn' => null,
],
[
'name' => 'افزونه فروش اقساطی',
'code' => 'ghesta',
@ -470,6 +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' => '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);
}
}

View file

@ -0,0 +1,490 @@
<?php
namespace App\Controller\Plugins;
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\Service\Access;
use App\Service\Log;
use App\Entity\Business;
use App\Entity\CustomInvoiceTemplate;
use App\Entity\PrintOptions;
use App\Service\CustomInvoice\TemplateRenderer;
use App\Service\Provider;
use Throwable;
class PlugCustomInvoice extends AbstractController
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
#[Route('/api/plugins/custominvoice/template', name: 'plugins_custominvoice_template_create', methods: ['POST'])]
public function createTemplate(Request $request, Access $access, Log $log, TemplateRenderer $renderer): JsonResponse
{
$acc = $access->hasRole('settings');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true) ?? [];
}
$name = $params['name'] ?? null;
$isPublic = (bool)($params['isPublic'] ?? false);
$code = $params['code'] ?? null;
if (!$name || !$code) {
return new JsonResponse(['status' => 'error', 'message' => 'name and code are required'], 400);
}
// Validate template before saving
try {
$renderer->render($code, $this->buildSampleContext($acc['bid']));
} catch (Throwable $e) {
return new JsonResponse([
'status' => 'error',
'message' => 'خطا در قالب: ' . $e->getMessage(),
'hint' => 'اگر قالب مشکل داشته باشد، در زمان چاپِ اسناد ممکن است هیچ خروجی تولید نشود.',
], 400);
}
$template = new CustomInvoiceTemplate();
$template->setBid($acc['bid']);
$template->setSubmitter($this->getUser());
$template->setName($name);
$template->setIsPublic($isPublic);
$template->setCode($code);
$this->entityManager->persist($template);
$this->entityManager->flush();
$log->insert('قالب فاکتور سفارشی', 'ایجاد قالب: ' . $name, $this->getUser(), $acc['bid']);
return $this->json([
'status' => 'ok',
'data' => [
'id' => $template->getId(),
'name' => $template->getName(),
'isPublic' => $template->isPublic(),
'code' => $template->getCode(),
],
]);
}
#[Route('/api/plugins/custominvoice/template/{id<\d+>}', name: 'plugins_custominvoice_template_get', methods: ['GET'])]
public function getTemplate(int $id, Access $access): JsonResponse
{
$acc = $access->hasRole('settings');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$template = $this->entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([
'id' => $id,
'bid' => $acc['bid'],
]);
if (!$template) {
return new JsonResponse(['status' => 'error', 'message' => 'Template not found'], 404);
}
return $this->json([
'status' => 'ok',
'data' => [
'id' => $template->getId(),
'name' => $template->getName(),
'isPublic' => $template->isPublic(),
'code' => $template->getCode(),
],
]);
}
#[Route('/api/plugins/custominvoice/template/{id<\d+>}', name: 'plugins_custominvoice_template_update', methods: ['PUT','POST'])]
public function updateTemplate(int $id, Request $request, Access $access, Log $log, TemplateRenderer $renderer): JsonResponse
{
$acc = $access->hasRole('settings');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$template = $this->entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([
'id' => $id,
'bid' => $acc['bid'],
]);
if (!$template) {
return new JsonResponse(['status' => 'error', 'message' => 'Template not found'], 404);
}
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true) ?? [];
}
if (array_key_exists('name', $params)) {
$template->setName((string)$params['name']);
}
if (array_key_exists('isPublic', $params)) {
$template->setIsPublic((bool)$params['isPublic']);
}
if (array_key_exists('code', $params)) {
$newCode = (string)$params['code'];
try {
$renderer->render($newCode, $this->buildSampleContext($acc['bid']));
} catch (Throwable $e) {
return new JsonResponse([
'status' => 'error',
'message' => 'خطا در قالب: ' . $e->getMessage(),
'hint' => 'اگر قالب مشکل داشته باشد، در زمان چاپِ اسناد ممکن است هیچ خروجی تولید نشود.',
], 400);
}
$template->setCode($newCode);
}
$this->entityManager->persist($template);
$this->entityManager->flush();
$log->insert('قالب فاکتور سفارشی', 'ویرایش قالب: ' . $template->getName(), $this->getUser(), $acc['bid']);
return $this->json([
'status' => 'ok',
'data' => [
'id' => $template->getId(),
'name' => $template->getName(),
'isPublic' => $template->isPublic(),
'code' => $template->getCode(),
],
]);
}
#[Route('/api/plugins/custominvoice/template/preview', name: 'plugins_custominvoice_template_preview', methods: ['POST'])]
public function preview(Request $request, Access $access, TemplateRenderer $renderer, Provider $provider): JsonResponse
{
$acc = $access->hasRole('settings');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true) ?? [];
$code = (string)($params['code'] ?? '');
$paper = (string)($params['paper'] ?? 'A4-L');
if ($code === '') {
return new JsonResponse(['status' => 'error', 'message' => 'code is required'], 400);
}
try {
$context = $this->buildSampleContext($acc['bid']);
$html = $renderer->render($code, $context);
} catch (Throwable $e) {
return new JsonResponse([
'status' => 'error',
'message' => 'خطا در قالب: ' . $e->getMessage(),
], 400);
}
$pid = $provider->createPrint($acc['bid'], $this->getUser(), $html, false, $paper);
$printUrl = $this->generateUrl('app_front_print', ['id' => $pid]);
return $this->json([
'status' => 'ok',
'html' => $html,
'printId' => $pid,
'printUrl' => $printUrl,
'warning' => 'این یک پیش فیمایش است. اگر قالب مشکل داشته باشد، هنگام چاپ اسناد ممکن است خروجی تولید نشود.',
]);
}
private function buildSampleContext(Business $bid): array
{
$now = time();
return [
'accountStatus' => [
'value' => 1250000,
'label' => 'بدهکار',
],
'bid' => [
'id' => method_exists($bid, 'getId') ? $bid->getId() : 0,
'legalName' => 'شرکت نمونه',
'shenasemeli' => '1234567890',
'shomaresabt' => '987654',
'codeeghtesadi' => '123456789012',
'tel' => '021-12345678',
'postalcode' => '1234567890',
'ostan' => 'تهران',
'shahrestan' => 'تهران',
'address' => 'خیابان نمونه، کوچه یک، پلاک 1',
],
'business' => [
'name' => 'کسب 4 کار نمونه',
'tel' => '021-12345678',
'mobile' => '09120000000',
'address' => 'تهران، ایران',
'shenasemeli' => '1234567890',
'codeeghtesadi' => '123456789012',
'id' => method_exists($bid, 'getId') ? $bid->getId() : 0,
],
'doc' => [
'code' => 'INV-0001',
'date' => date('Y/m/d', $now),
'taxPercent' => 9,
'discountPercent' => 5,
'discountType' => 'amount',
'amount' => 2350000,
'relatedDocs' => [
['date' => date('Y/m/d', $now), 'amount' => 500000, 'des' => 'پرداخت شماره 1'],
],
'money' => [
'shortName' => 'ریال',
],
],
'rows' => [
[
'commodity' => [
'code' => 'P-001',
'name' => 'کالای A',
'unit' => ['name' => 'عدد'],
],
'commodityCount' => 2,
'des' => 'شرح آیتم اول',
'bs' => 1000000,
'tax' => 90000,
'discount' => 50000,
'showPercentDiscount' => false,
'discountPercent' => 0,
],
[
'commodity' => [
'code' => 'S-002',
'name' => 'خدمت B',
'unit' => ['name' => 'ساعت'],
],
'commodityCount' => 3,
'des' => 'شرح آیتم دوم',
'bs' => 1350000,
'tax' => 121500,
'discount' => 0,
'showPercentDiscount' => true,
'discountPercent' => 10,
],
],
'person' => [
'prelabel' => ['label' => 'جناب'],
'nikename' => 'مشتری نمونه',
'shenasemeli' => '1234567890',
'sabt' => '112233',
'codeeghtesadi' => '556677889900',
'tel' => '021-88888888',
'postalcode' => '1234567890',
'ostan' => 'تهران',
'shahr' => 'تهران',
'address' => 'خیابان مشتری، پلاک 10',
],
'discount' => 50000,
'transfer' => 20000,
'printOptions' => [
'invoiceIndex' => true,
'discountInfo' => true,
'taxInfo' => true,
'pays' => true,
'note' => true,
'businessStamp' => true,
'paper' => 'A4-L',
],
'note' => '<b>یادداشت آزمایشی:</b> این فقط پیش فیمایش است.',
];
}
#[Route('/api/plugins/custominvoice/template/{id<\d+>}/copy', name: 'plugins_custominvoice_template_copy', methods: ['POST'])]
public function copyTemplate(int $id, Request $request, Access $access, Log $log): JsonResponse
{
$acc = $access->hasRole('settings');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$source = $this->entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([
'id' => $id,
]);
if (!$source || !$source->isPublic()) {
return new JsonResponse(['status' => 'error', 'message' => 'Source template not found or not public'], 404);
}
$new = new CustomInvoiceTemplate();
$new->setBid($acc['bid']);
$new->setSubmitter($this->getUser());
$new->setName($source->getName());
$new->setIsPublic(false);
$new->setCode($source->getCode());
$this->entityManager->persist($new);
// increment source copy count
$source->setCopyCount($source->getCopyCount() + 1);
$this->entityManager->persist($source);
$this->entityManager->flush();
$log->insert('قالب فاکتور سفارشی', 'کپی از قالب عمومی: ' . $source->getName(), $this->getUser(), $acc['bid']);
return $this->json([
'status' => 'ok',
'data' => [
'id' => $new->getId(),
]
]);
}
#[Route('/api/plugins/custominvoice/template/list', name: 'plugins_custominvoice_template_list', methods: ['POST'])]
public function listTemplates(Request $request, Access $access): JsonResponse
{
$acc = $access->hasRole('settings');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true) ?? [];
$page = max(1, (int)($params['page'] ?? 1));
$limit = max(1, min(100, (int)($params['itemsPerPage'] ?? 10)));
$search = trim((string)($params['search'] ?? ''));
$sortBy = $params['sortBy'] ?? [];
$scope = strtolower((string)($params['scope'] ?? 'business'));
if (!in_array($scope, ['business', 'public'], true)) {
$scope = 'business';
}
$qb = $this->entityManager->getRepository(CustomInvoiceTemplate::class)
->createQueryBuilder('t')
->leftJoin('t.submitter', 'u')
->addSelect('u');
if ($scope === 'business') {
$qb->andWhere('t.bid = :bid')
->setParameter('bid', $acc['bid']);
} else {
$qb->andWhere('t.isPublic = 1');
}
if ($search !== '') {
$qb->andWhere(
$qb->expr()->orX(
't.name LIKE :search',
't.code LIKE :search',
'u.fullName LIKE :search',
'u.email LIKE :search'
)
)->setParameter('search', '%' . $search . '%');
}
if (is_array($sortBy) && count($sortBy) > 0) {
$firstSort = $sortBy[0];
$key = in_array($firstSort['key'] ?? 'id', ['id', 'name', 'isPublic', 'copyCount']) ? $firstSort['key'] : 'id';
$order = strtoupper($firstSort['order'] ?? 'DESC');
$order = $order === 'ASC' ? 'ASC' : 'DESC';
$qb->orderBy('t.' . $key, $order);
} else {
// default: for public scope, popular first; else by id desc
if ($scope === 'public') {
$qb->orderBy('t.copyCount', 'DESC')->addOrderBy('t.id', 'DESC');
} else {
$qb->orderBy('t.id', 'DESC');
}
}
$total = (int)(clone $qb)->select('COUNT(t.id)')->getQuery()->getSingleScalarResult();
$items = $qb->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit)
->getQuery()
->getResult();
$currentBidId = method_exists($acc['bid'], 'getId') ? $acc['bid']->getId() : null;
$data = array_map(function (CustomInvoiceTemplate $t) use ($currentBidId) {
$ownedByMe = $currentBidId !== null && $t->getBid() && $t->getBid()->getId() === $currentBidId;
return [
'id' => $t->getId(),
'name' => $t->getName(),
'isPublic' => $t->isPublic(),
'code' => $t->getCode(),
'ownedByMe' => $ownedByMe,
'submitter' => $t->getSubmitter() ? [
'id' => $t->getSubmitter()->getId(),
'fullName' => $t->getSubmitter()->getFullName(),
'email' => $t->getSubmitter()->getEmail(),
] : null,
'copyCount' => $t->getCopyCount(),
'popular' => $t->getCopyCount() > 10,
];
}, $items);
return $this->json([
'items' => $data,
'total' => $total,
'page' => $page,
'itemsPerPage' => $limit,
]);
}
#[Route('/api/plugins/custominvoice/template/{id<\d+>}', name: 'plugins_custominvoice_template_delete', methods: ['DELETE'])]
public function deleteTemplate(int $id, Access $access, Log $log): JsonResponse
{
$acc = $access->hasRole('settings');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$template = $this->entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([
'id' => $id,
'bid' => $acc['bid'],
]);
if (!$template) {
return new JsonResponse(['status' => 'error', 'message' => 'Template not found'], 404);
}
// Before delete, nullify references in PrintOptions for this business
$printOptionsList = $this->entityManager->getRepository(PrintOptions::class)->findBy([
'bid' => $acc['bid'],
]);
foreach ($printOptionsList as $opt) {
$changed = false;
if ($opt->getSellTemplate() && $opt->getSellTemplate()->getId() === $template->getId()) {
$opt->setSellTemplate(null);
$changed = true;
}
if ($opt->getBuyTemplate() && $opt->getBuyTemplate()->getId() === $template->getId()) {
$opt->setBuyTemplate(null);
$changed = true;
}
if ($opt->getRfbuyTemplate() && $opt->getRfbuyTemplate()->getId() === $template->getId()) {
$opt->setRfbuyTemplate(null);
$changed = true;
}
if ($opt->getRfsellTemplate() && $opt->getRfsellTemplate()->getId() === $template->getId()) {
$opt->setRfsellTemplate(null);
$changed = true;
}
if ($changed) {
$this->entityManager->persist($opt);
}
}
// Apply changes before deleting the template to avoid FK issues when migrations not applied
$this->entityManager->flush();
$name = $template->getName();
$this->entityManager->remove($template);
$this->entityManager->flush();
$log->insert('قالب فاکتور سفارشی', 'حذف قالب: ' . $name, $this->getUser(), $acc['bid']);
return $this->json(['status' => 'ok']);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,15 @@
<?php
/**
* 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;
@ -16,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
@ -27,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
{
@ -37,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
{
@ -80,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]);
@ -88,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();
@ -138,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
{
@ -240,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
{
@ -260,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);
@ -377,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');
@ -422,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
{
@ -550,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));
@ -557,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;
@ -569,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,
@ -733,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
{
@ -743,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([
@ -767,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' => 'تنظیمات مالیاتی تکمیل نشده است. لطفاً ابتدا تنظیمات را تکمیل کنید.'
@ -778,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([
@ -838,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 فاکتور');
}
@ -894,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
{
@ -950,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
{
@ -970,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' => 'تنظیمات مالیاتی تکمیل نشده است. لطفاً ابتدا تنظیمات را تکمیل کنید.'
@ -983,8 +1255,8 @@ class TaxSettingsController extends AbstractController
}
try {
$username = $taxSettings->getTaxMemoryId();
$privateKey = $taxSettings->getPrivateKey();
$username = $taxSettings['taxMemoryId'];
$privateKey = $taxSettings['privateKey'];
$taxOrgPublicKey = '';
$taxOrgKeyId = '';
@ -1157,22 +1429,66 @@ class TaxSettingsController extends AbstractController
$totalTax += $item['tax'];
}
$buyerNationalId = null;
$buyerEconomicCode = null;
$buyerPostalCode = null;
$buyerPerson = null;
foreach ($invoice->getHesabdariRows() as $row) {
if ($row->getPerson()) {
$buyerPerson = $row->getPerson();
break;
}
}
if ($buyerPerson) {
$buyerNationalId = $buyerPerson->getShenasemeli();
$buyerEconomicCode = $buyerPerson->getCodeeghtesadi();
$buyerPostalCode = $buyerPerson->getPostalCode();
if (empty($buyerNationalId) || trim($buyerNationalId) === '') {
$buyerNationalId = null;
}
if (empty($buyerEconomicCode) || trim($buyerEconomicCode) === '') {
$buyerEconomicCode = null;
}
if (empty($buyerPostalCode) || trim($buyerPostalCode) === '') {
$buyerPostalCode = null;
}
}
$personType = 1;
if (strlen($buyerNationalId) == 11) {
$personType = 2;
}
$InvoiceType = 2;
$invoicePayType = null;
if ($buyerNationalId && $buyerEconomicCode) {
$InvoiceType = 1;
$invoicePayType = 1;
}
$dateTime = new DateTime();
$header = (new \SnappMarketPro\Moadian\Dto\InvoiceHeaderDto())
->setTaxid($moadian->generateTaxId($dateTime, $internalId))
->setIndati2m($dateTime->getTimestamp() * 1000)
->setIndatim($dateTime->getTimestamp() * 1000)
->setInty(2)
->setInty($InvoiceType)
->setInno($moadian->normalizeInvoiceNumber($internalId))
->setIrtaxid(null)
->setInp(1)
->setIns(1)
->setTins($taxId)
->setTob(1)
->setBid(null)
->setTinb(null)
->setTob($personType)
->setBid($buyerNationalId)
->setTinb($buyerEconomicCode)
->setSbc(null)
->setBpc(null)
->setBpc($buyerPostalCode)
->setBbc(null)
->setFt(null)
->setBpn(null)
@ -1180,13 +1496,13 @@ 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)
->setTodam($data['shippingCost'])
->setTbill($data['finalTotal'])
->setSetm(null)
->setSetm($invoicePayType)
->setCap(null)
->setInsp(null)
->setTvop(null)
@ -1195,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'])
@ -1208,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)
@ -1226,7 +1552,7 @@ class TaxSettingsController extends AbstractController
->setCop(null)
->setVop(null)
->setBsrn(null)
->setTsstam($itemTotal);
->setTsstam($os);
$bodyItems[] = $bodyDto;
}
@ -1251,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
{
@ -1271,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' => 'تنظیمات مالیاتی تکمیل نشده است. لطفاً ابتدا تنظیمات را تکمیل کنید.'
@ -1284,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([
@ -1394,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,
@ -1483,5 +1840,178 @@ 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
{
$acc = $access->hasRole('plugTaxSettings');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$user = $this->getUser();
$taxSettings = $this->getTaxSettings($em, $businessId, $user);
if (!$taxSettings['privateKey'] || !$taxSettings['taxMemoryId']) {
return $this->json([
'success' => false,
'message' => 'تنظیمات مالیاتی تکمیل نشده است. لطفاً ابتدا تنظیمات را تکمیل کنید.'
]);
}
$taxInvoiceRepo = $em->getRepository(PluginTaxInvoice::class);
$taxInvoice = $taxInvoiceRepo->findOneBy([
'id' => $id,
'business' => $businessId
]);
if (!$taxInvoice) {
return $this->json([
'success' => false,
'message' => 'فاکتور مالیاتی یافت نشد.'
]);
}
$invoice = $taxInvoice->getInvoice();
if (!$invoice) {
return $this->json([
'success' => false,
'message' => 'فاکتور یافت نشد.'
]);
}
if ($invoice->getBid()->getId() != $businessId) {
return $this->json([
'success' => false,
'message' => 'فاکتور متعلق به این کسب و کار نیست.'
]);
}
$buyerInfo = $this->validateBuyerEconomicInfo($invoice);
if (!$buyerInfo['is_valid']) {
return $this->json([
'success' => false,
'message' => 'اطلاعات اقتصادی خریدار ناقص است.',
'buyer_info' => $buyerInfo,
'can_proceed' => false
]);
}
return $this->json([
'success' => true,
'message' => 'اطلاعات اقتصادی خریدار کامل است.',
'buyer_info' => $buyerInfo,
'can_proceed' => true
]);
}
/**
* بررسی اطلاعات اقتصادی خریدار
*/
private function validateBuyerEconomicInfo($invoice): array
{
$buyerPerson = null;
$buyerNationalId = null;
$buyerEconomicCode = null;
$missingFields = [];
foreach ($invoice->getHesabdariRows() as $row) {
if ($row->getPerson()) {
$buyerPerson = $row->getPerson();
break;
}
}
if (!$buyerPerson) {
return [
'is_valid' => false,
'message' => 'خریدار در فاکتور مشخص نشده است.',
'buyer_name' => null,
'national_id' => null,
'economic_code' => null,
'missing_fields' => ['buyer_not_found']
];
}
$buyerNationalId = $buyerPerson->getShenasemeli();
$buyerEconomicCode = $buyerPerson->getCodeeghtesadi();
// بررسی شناسه ملی
if (empty($buyerNationalId) || trim($buyerNationalId) === '') {
$missingFields[] = 'national_id';
}
// بررسی کد اقتصادی
if (empty($buyerEconomicCode) || trim($buyerEconomicCode) === '') {
$missingFields[] = 'economic_code';
}
$result = [
'is_valid' => empty($missingFields),
'buyer_name' => $buyerPerson->getNikename(),
'buyer_id' => $buyerPerson->getId(),
'buyer_code' => $buyerPerson->getCode(),
'national_id' => $buyerNationalId,
'economic_code' => $buyerEconomicCode,
'missing_fields' => $missingFields
];
if (!empty($missingFields)) {
$missingFieldsText = [];
if (in_array('national_id', $missingFields)) {
$missingFieldsText[] = 'شناسه ملی';
}
if (in_array('economic_code', $missingFields)) {
$missingFieldsText[] = 'کد اقتصادی';
}
$result['message'] = 'اطلاعات اقتصادی خریدار ناقص است. فیلدهای زیر تکمیل نشده‌اند: ' . implode('، ', $missingFieldsText);
} else {
$result['message'] = 'اطلاعات اقتصادی خریدار کامل است.';
}
return $result;
}
}

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

@ -6,6 +6,7 @@ use App\Entity\Printer;
use App\Entity\PrinterQueue;
use App\Entity\PrintItem;
use App\Entity\PrintOptions;
use App\Entity\CustomInvoiceTemplate;
use App\Service\Access;
use App\Service\Explore;
use App\Service\Extractor;
@ -59,6 +60,7 @@ class PrintersController extends AbstractController
$temp['sell']['paper'] = $settings->getSellPaper();
$temp['sell']['businessStamp'] = $settings->isSellBusinessStamp();
$temp['sell']['invoiceIndex'] = $settings->isSellInvoiceIndex();
$temp['sell']['templateId'] = $settings->getSellTemplate() ? $settings->getSellTemplate()->getId() : null;
if (!$temp['sell']['paper']) {
$temp['sell']['paper'] = 'A4-L';
}
@ -71,6 +73,7 @@ class PrintersController extends AbstractController
$temp['buy']['noteString'] = $settings->getBuyNoteString();
$temp['buy']['pays'] = $settings->isBuyPays();
$temp['buy']['paper'] = $settings->getBuyPaper();
$temp['buy']['templateId'] = $settings->getBuyTemplate() ? $settings->getBuyTemplate()->getId() : null;
if (!$temp['buy']['paper']) {
$temp['buy']['paper'] = 'A4-L';
}
@ -83,6 +86,7 @@ class PrintersController extends AbstractController
$temp['rfbuy']['noteString'] = $settings->getRfbuyNoteString();
$temp['rfbuy']['pays'] = $settings->isRfbuyPays();
$temp['rfbuy']['paper'] = $settings->getRfbuyPaper();
$temp['rfbuy']['templateId'] = $settings->getRfbuyTemplate() ? $settings->getRfbuyTemplate()->getId() : null;
if (!$temp['rfbuy']['paper']) {
$temp['rfbuy']['paper'] = 'A4-L';
}
@ -95,6 +99,7 @@ class PrintersController extends AbstractController
$temp['rfsell']['noteString'] = $settings->getRfsellNoteString();
$temp['rfsell']['pays'] = $settings->isRfsellPays();
$temp['rfsell']['paper'] = $settings->getRfsellPaper();
$temp['rfsell']['templateId'] = $settings->getRfsellTemplate() ? $settings->getRfsellTemplate()->getId() : null;
$temp['fastsell']['cashdeskTicket'] = $settings->isFastsellCashdeskTicket();
$temp['fastsell']['invoice'] = $settings->isFastsellInvoice();
@ -139,6 +144,19 @@ class PrintersController extends AbstractController
$settings->setSellPaper($params['sell']['paper']);
$settings->setSellBusinessStamp($params['sell']['businessStamp'] ?? false);
$settings->setSellInvoiceIndex($params['sell']['invoiceIndex'] ?? false);
// Resolve templates by ID and ownership; update only if key exists
if (array_key_exists('sell', $params) && array_key_exists('templateId', $params['sell'])) {
$sellT = null;
if ($params['sell']['templateId']) {
$sellT = $entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([
'id' => (int)$params['sell']['templateId'],
'bid' => $acc['bid'],
]);
}
$settings->setSellTemplate($sellT);
}
if ($params['buy']['bidInfo'] == null) {
$settings->setBuyBidInfo(false);
} else {
@ -152,6 +170,17 @@ class PrintersController extends AbstractController
$settings->setBuyPays($params['buy']['pays'] ?? false);
$settings->setBuyPaper($params['buy']['paper']);
if (array_key_exists('buy', $params) && array_key_exists('templateId', $params['buy'])) {
$buyT = null;
if ($params['buy']['templateId']) {
$buyT = $entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([
'id' => (int)$params['buy']['templateId'],
'bid' => $acc['bid'],
]);
}
$settings->setBuyTemplate($buyT);
}
$settings->setRfbuyBidInfo($params['rfbuy']['bidInfo'] ?? false);
$settings->setRfbuyTaxInfo($params['rfbuy']['taxInfo'] ?? false);
$settings->setRfbuyDiscountInfo($params['rfbuy']['discountInfo'] ?? false);
@ -160,6 +189,17 @@ class PrintersController extends AbstractController
$settings->setRfbuyPays($params['rfbuy']['pays'] ?? false);
$settings->setRfbuyPaper($params['rfbuy']['paper']);
if (array_key_exists('rfbuy', $params) && array_key_exists('templateId', $params['rfbuy'])) {
$rfbuyT = null;
if ($params['rfbuy']['templateId']) {
$rfbuyT = $entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([
'id' => (int)$params['rfbuy']['templateId'],
'bid' => $acc['bid'],
]);
}
$settings->setRfbuyTemplate($rfbuyT);
}
$settings->setRfsellBidInfo($params['rfsell']['bidInfo'] ?? false);
$settings->setRfsellTaxInfo($params['rfsell']['taxInfo'] ?? false);
$settings->setRfsellDiscountInfo($params['rfsell']['discountInfo'] ?? false);
@ -168,6 +208,17 @@ class PrintersController extends AbstractController
$settings->setRfsellPays($params['rfsell']['pays'] ?? false);
$settings->setRfSellPaper($params['rfsell']['paper']);
if (array_key_exists('rfsell', $params) && array_key_exists('templateId', $params['rfsell'])) {
$rfsellT = null;
if ($params['rfsell']['templateId']) {
$rfsellT = $entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([
'id' => (int)$params['rfsell']['templateId'],
'bid' => $acc['bid'],
]);
}
$settings->setRfsellTemplate($rfsellT);
}
$settings->setRepserviceNoteString($params['repservice']['noteString']);
$settings->setRepServicePaper($params['repservice']['paper']);

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

@ -14,6 +14,8 @@ use App\Entity\HesabdariTable;
use App\Entity\InvoiceType;
use App\Entity\Person;
use App\Entity\PrintOptions;
use App\Entity\CustomInvoiceTemplate;
use App\Service\CustomInvoice\TemplateRenderer;
use App\Entity\StoreroomTicket;
use App\Service\Printers;
use Doctrine\ORM\EntityManagerInterface;
@ -386,7 +388,7 @@ class RfbuyController extends AbstractController
}
#[Route('/api/rfbuy/print/invoice', name: 'app_rfbuy_print_invoice')]
public function app_rfbuy_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
public function app_rfbuy_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, TemplateRenderer $renderer): JsonResponse
{
$params = [];
if ($content = $request->getContent()) {
@ -447,20 +449,86 @@ class RfbuyController extends AbstractController
$note = '';
$printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid'=>$acc['bid']]);
if($printSettings){$note = $printSettings->getRfbuyNoteString();}
$pdfPid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$this->renderView('pdf/printers/rfbuy.html.twig', [
// Build safe context
$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(),
];
}, $doc->getHesabdariRows()->toArray());
$personArr = $person ? [
'name' => $person->getName(),
'mobile' => $person->getMobile(),
'tel' => $person->getTel(),
'address' => $person->getAddress(),
] : null;
$biz = $acc['bid'];
$businessArr = $biz ? [
'name' => method_exists($biz, 'getName') ? $biz->getName() : null,
'tel' => method_exists($biz, 'getTel') ? $biz->getTel() : null,
'mobile' => method_exists($biz, 'getMobile') ? $biz->getMobile() : null,
'address' => method_exists($biz, 'getAddress') ? $biz->getAddress() : null,
'shenasemeli' => method_exists($biz, 'getShenasemeli') ? $biz->getShenasemeli() : null,
'codeeghtesadi' => method_exists($biz, 'getCodeeghtesadi') ? $biz->getCodeeghtesadi() : null,
] : null;
$context = [
'business' => $businessArr,
'doc' => [
'code' => $doc->getCode(),
'date' => method_exists($doc, 'getDate') ? $doc->getDate() : null,
],
'rows' => $rowsArr,
'person' => $personArr,
'discount' => $discount,
'transfer' => $transfer,
'printOptions'=> $printOptions,
'note'=> $note
];
$html = null;
$selectedTemplate = $printSettings ? $printSettings->getRfbuyTemplate() : null;
if ($selectedTemplate instanceof CustomInvoiceTemplate) {
$html = $renderer->render($selectedTemplate->getCode() ?? '', $context);
}
if ($html === null) {
$html = $this->renderView('pdf/printers/rfbuy.html.twig', [
'bid' => $acc['bid'],
'doc' => $doc,
'rows' => $doc->getHesabdariRows(),
'rows' => array_map(function ($row) {
return [
'commodity' => $row->getCommodity(),
'commodityCount' => $row->getCommdityCount(),
'commdityCount' => $row->getCommdityCount(),
'des' => $row->getDes(),
'bs' => $row->getBs(),
'bd' => $row->getBd(),
'tax' => $row->getTax(),
'discount' => $row->getDiscount(),
];
}, $doc->getHesabdariRows()->toArray()),
'person' => $person,
'printInvoice' => $params['printers'],
'discount' => $discount,
'transfer' => $transfer,
'printOptions'=> $printOptions,
'note'=> $note
]),
]);
}
$pdfPid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$html,
false,
$printOptions['paper']
);
@ -472,7 +540,18 @@ class RfbuyController extends AbstractController
$this->renderView('pdf/posPrinters/justRfbuy.html.twig', [
'bid' => $acc['bid'],
'doc' => $doc,
'rows' => $doc->getHesabdariRows(),
'rows' => array_map(function ($row) {
return [
'commodity' => $row->getCommodity(),
'commodityCount' => $row->getCommdityCount(),
'commdityCount' => $row->getCommdityCount(),
'des' => $row->getDes(),
'bs' => $row->getBs(),
'bd' => $row->getBd(),
'tax' => $row->getTax(),
'discount' => $row->getDiscount(),
];
}, $doc->getHesabdariRows()->toArray()),
]),
false
);

View file

@ -14,6 +14,8 @@ use App\Entity\HesabdariTable;
use App\Entity\InvoiceType;
use App\Entity\Person;
use App\Entity\PrintOptions;
use App\Entity\CustomInvoiceTemplate;
use App\Service\CustomInvoice\TemplateRenderer;
use App\Entity\StoreroomTicket;
use App\Service\Printers;
use Doctrine\ORM\EntityManagerInterface;
@ -408,7 +410,7 @@ class RfsellController extends AbstractController
}
#[Route('/api/rfsell/print/invoice', name: 'app_rfsell_print_invoice')]
public function app_rfsell_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
public function app_rfsell_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, TemplateRenderer $renderer): JsonResponse
{
$params = [];
if ($content = $request->getContent()) {
@ -469,20 +471,87 @@ class RfsellController extends AbstractController
$note = '';
$printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid'=>$acc['bid']]);
if($printSettings){$note = $printSettings->getRfsellNoteString();}
$pdfPid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$this->renderView('pdf/printers/rfsell.html.twig', [
// Build safe context
$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(),
];
}, $doc->getHesabdariRows()->toArray());
$personArr = $person ? [
'name' => $person->getName(),
'mobile' => $person->getMobile(),
'tel' => $person->getTel(),
'address' => $person->getAddress(),
] : null;
$biz = $acc['bid'];
$businessArr = $biz ? [
'name' => method_exists($biz, 'getName') ? $biz->getName() : null,
'tel' => method_exists($biz, 'getTel') ? $biz->getTel() : null,
'mobile' => method_exists($biz, 'getMobile') ? $biz->getMobile() : null,
'address' => method_exists($biz, 'getAddress') ? $biz->getAddress() : null,
'shenasemeli' => method_exists($biz, 'getShenasemeli') ? $biz->getShenasemeli() : null,
'codeeghtesadi' => method_exists($biz, 'getCodeeghtesadi') ? $biz->getCodeeghtesadi() : null,
] : null;
$context = [
'business' => $businessArr,
'doc' => [
'code' => $doc->getCode(),
'date' => method_exists($doc, 'getDate') ? $doc->getDate() : null,
],
'rows' => $rowsArr,
'person' => $personArr,
'discount' => $discount,
'transfer' => $transfer,
'printOptions'=> $printOptions,
'note'=> $note
];
$html = null;
$selectedTemplate = $printSettings ? $printSettings->getRfsellTemplate() : null;
if ($selectedTemplate instanceof CustomInvoiceTemplate) {
$html = $renderer->render($selectedTemplate->getCode() ?? '', $context);
}
if ($html === null) {
$html = $this->renderView('pdf/printers/rfsell.html.twig', [
'bid' => $acc['bid'],
'doc' => $doc,
'rows' => $doc->getHesabdariRows(),
'rows' => array_map(function ($row) {
return [
'commodity' => $row->getCommodity(),
'commodityCount' => $row->getCommdityCount(),
'commdityCount' => $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
]),
]);
}
$pdfPid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$html,
false,
$printOptions['paper']
);
@ -494,7 +563,21 @@ class RfsellController extends AbstractController
$this->renderView('pdf/posPrinters/justSell.html.twig', [
'bid' => $acc['bid'],
'doc' => $doc,
'rows' => $doc->getHesabdariRows(),
'rows' => array_map(function ($row) {
return [
'commodity' => $row->getCommodity(),
'commodityCount' => $row->getCommdityCount(),
'commdityCount' => $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,
'transfer' => $transfer,
]),
false
);

View file

@ -14,6 +14,9 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class SalaryController extends AbstractController
{
@ -43,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();
@ -66,7 +77,29 @@ class SalaryController extends AbstractController
'bid' => $acc['bid'],
'code' => $code
]);
return $this->json(Explore::ExploreSalary($data));
$result = Explore::ExploreSalary($data);
// محاسبه بدهکار و بستانکار و تراز
$bs = 0;
$bd = 0;
// 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();
}
$result['bs'] = $bs;
$result['bd'] = $bd;
$result['balance'] = $bd - $bs;
return $this->json($result);
}
#[Route('/api/salary/mod/{code}', name: 'app_salary_mod')]
@ -124,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]);
@ -170,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();
@ -204,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();
@ -244,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'])
@ -267,4 +334,175 @@ class SalaryController extends AbstractController
'total' => count($transactions)
]);
}
/**
* خروجی اکسل کارت حساب تنخواه گردان
*/
#[Route('/api/salary/card/list/excel', name: 'app_salary_card_list_excel')]
public function app_salary_card_list_excel(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): BinaryFileResponse|JsonResponse|Response
{
$acc = $access->hasRole('salary');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('code', $params))
throw $this->createNotFoundException();
$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)) {
$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'])) {
foreach ($params['items'] as $param) {
$id = is_array($param) ? ($param['id'] ?? null) : $param;
if ($id !== null) {
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'id' => $id,
'bid' => $acc['bid'],
'salary' => $salary,
'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 = [
[
'شماره تراکنش',
'تاریخ',
'توضیحات',
'تفضیل',
'بستانکار',
'بدهکار',
'سال مالی',
]
];
foreach ($transactions as $transaction) {
$arrayEntity[] = [
$transaction->getId(),
$transaction->getDoc()->getDate(),
$transaction->getDes(),
$transaction->getRef() ? $transaction->getRef()->getName() : '',
$transaction->getBs(),
$transaction->getBd(),
$transaction->getYear() ? $transaction->getYear()->getlabel() : '',
];
}
$activeWorksheet->fromArray($arrayEntity, null, 'A1');
$activeWorksheet->setRightToLeft(true);
$writer = new Xlsx($spreadsheet);
$filePath = __DIR__ . '/../../var/' . uniqid('salary_card_', true) . '.xlsx';
$writer->save($filePath);
return new BinaryFileResponse($filePath);
}
/**
* خروجی PDF کارت حساب تنخواه گردان
*/
#[Route('/api/salary/card/list/print', name: 'app_salary_card_list_print')]
public function app_salary_card_list_print(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('salary');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('code', $params))
throw $this->createNotFoundException();
$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)) {
$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'])) {
foreach ($params['items'] as $param) {
$id = is_array($param) ? ($param['id'] ?? null) : $param;
if ($id !== null) {
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'id' => $id,
'bid' => $acc['bid'],
'salary' => $salary,
'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(),
$this->renderView('pdf/salary_card.html.twig', [
'page_title' => 'کارت حساب تنخواه گردان ' . $salary->getName(),
'bid' => $acc['bid'],
'items' => $transactions,
'salary' => $salary
])
);
return $this->json(['id' => $pid]);
}
}

View file

@ -2,13 +2,13 @@
namespace App\Controller;
use App\Entity\Business;
use App\Service\AccountingPermissionService;
use App\Service\Jdate;
use App\Service\Log;
use App\Service\Access;
use App\Service\Explore;
use App\Entity\Commodity;
use App\Service\PluginService;
use App\Service\Provider;
use App\Service\Extractor;
use App\Entity\HesabdariDoc;
@ -31,6 +31,9 @@ use App\Entity\BankAccount;
use App\Entity\Cashdesk;
use App\Entity\Salary;
use App\Entity\Year;
use App\Entity\CustomInvoiceTemplate;
use App\Service\CustomInvoice\TemplateRenderer;
use App\Service\PluginService;
class SellController extends AbstractController
{
@ -42,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']
@ -66,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
{
@ -125,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
@ -422,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')
@ -487,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
];
@ -533,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)
@ -619,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 {
@ -644,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 {
@ -710,7 +771,7 @@ class SellController extends AbstractController
}
#[Route('/api/sell/print/invoice', name: 'app_sell_print_invoice')]
public function app_sell_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
public function app_sell_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, PluginService $pluginService, TemplateRenderer $renderer): JsonResponse
{
$acc = $access->hasRole('sell');
if (!$acc)
@ -776,15 +837,89 @@ 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) {
$note = $printSettings->getSellNoteString();
}
$pdfPid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$this->renderView('pdf/printers/sell.html.twig', [
// 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;
$biz = $acc['bid'];
$businessArr = $biz ? [
'name' => method_exists($biz, 'getName') ? $biz->getName() : null,
'tel' => method_exists($biz, 'getTel') ? $biz->getTel() : null,
'mobile' => method_exists($biz, 'getMobile') ? $biz->getMobile() : null,
'address' => method_exists($biz, 'getAddress') ? $biz->getAddress() : null,
'shenasemeli' => method_exists($biz, 'getShenasemeli') ? $biz->getShenasemeli() : null,
'codeeghtesadi' => method_exists($biz, 'getCodeeghtesadi') ? $biz->getCodeeghtesadi() : null,
'id' => method_exists($biz, 'getId') ? $biz->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,
];
// Decide template: custom or default
$html = null;
$isCustomInvoiceActive = $pluginService->isActive('custominvoice', $acc['bid']);
$selectedTemplate = $printSettings ? $printSettings->getSellTemplate() : null;
if ($isCustomInvoiceActive && $selectedTemplate instanceof CustomInvoiceTemplate) {
$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' => $acc['bid'],
'doc' => $doc,
@ -808,7 +943,13 @@ class SellController extends AbstractController
'note' => $note,
'showPercentDiscount' => $doc->getDiscountType() === 'percent',
'discountPercent' => $doc->getDiscountPercent()
]),
]);
}
$pdfPid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$html,
false,
$printOptions['paper']
);
@ -916,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'])) {
@ -957,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());
}
}
// تنظیم اطلاعات اصلی فاکتور
@ -1140,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);
@ -1196,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);
@ -1236,6 +1400,7 @@ class SellController extends AbstractController
}
return $this->json([
'Success' => 1,
'result' => 1,
'message' => 'فاکتور با موفقیت ثبت شد',
'data' => [
@ -1267,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,

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