Compare commits

..

168 commits

Author SHA1 Message Date
Hesabix 8d528f70c0 some bug fix and login with email 2025-11-06 17:36:02 +00:00
Hesabix 9ad415a23b add profit report 2025-11-02 12:23:19 +00:00
Hesabix 9074059abd some bug fixes 2025-11-02 10:08:30 +00:00
Hesabix a579a020bc bug fix in print invoice sell type 2025-10-23 10:37:42 +03:30
Hesabix 98645b24c3 add person info in pos invoice 2025-10-22 20:03:03 +03:30
Hesabix 7a87159482 add app info to footer of invoice pos print 2025-10-22 19:22:50 +03:30
Hesabix fb037a8299 bug fix access control 2025-10-01 15:51:33 +03:30
Hesabix 61ff2d4ca7 more bug fix 2025-10-01 14:08:55 +03:30
Hesabix 5a1348a800 Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-10-01 13:42:17 +03:30
Hesabix 29e755fd2c some bug fix 2025-10-01 13:40:14 +03:30
Hesabix b64fd56d1c Update for Moadian plugin 2025-09-27 13:34:09 +03:30
Hesabix fab5a76f58 Update for Moadian plugin 2025-09-27 13:24:27 +03:30
Hesabix b8ad5f2f49 change text of ticket invoice in storeroom 2025-09-03 11:40:27 +03:30
Hesabix 5893c5b196 bug fix in remove documents 2025-09-03 03:41:02 +03:30
Hesabix 1cef78c6f5 almost done two step approval in persons part 2025-09-03 03:13:49 +03:30
Hesabix 3d454a642f progress in two step approval 2025-09-03 02:27:31 +03:30
Hesabix 822402cfda add notic for cheques list with two colors 2025-09-03 01:04:56 +03:30
Hesabix fb251af713 redesign rfsell part 2025-09-03 00:48:42 +03:30
Hesabix ecfebfad6e bug fix in tafsil word 2025-09-02 23:31:56 +03:30
Hesabix d7a258d9a6 bug fix in edit sell doc 2025-09-02 17:10:09 +03:30
Hesabix 3e477a6137 bug fin in create two step approval 2025-08-30 19:39:42 -04:00
Hesabix c722fb2c34 bug fix in none https sites for captcha resolve 2025-08-30 19:10:32 -04:00
Gloomy 3d490ffe51 update importWorkflow plugin/fix storeroom pdf print 2025-08-30 13:35:38 +00:00
Hesabix 0fb64e8cfa Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-28 12:16:32 +00:00
Hesabix b2766b6d46 bug fix in generate accounting codes 2025-08-28 12:16:28 +00:00
Gloomy 6d3832f682 update importWorkflow plugin 2025-08-28 11:38:22 +00:00
Gloomy 663f5f7173 update importWorkflow plugin 2025-08-28 11:32:50 +00:00
Hesabix f37aca7c6e bug fix in cheques 2025-08-25 08:48:16 +00:00
Hesabix b474a9817b bug fix in permissions 2025-08-25 08:10:48 +00:00
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
Hesabix 68ef03e863 bug fix in dto 2025-07-23 16:43:30 +00:00
Hesabix 23c6775f60 bug fix in plugins sync 2025-07-23 15:32:30 +00:00
Hesabix 9113a15194 Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-07-23 14:24:17 +00:00
Hesabix a638f22e3c update plugins sync and database default file for init 2025-07-23 14:21:39 +00:00
Gloomy 0be44cd46a Refactor tax settings save endpoint to use DTO and validation 2025-07-23 12:44:10 +00:00
Hesabix aa02ea0925 Merge pull request 'master' (#1) from Gloomy/hesabixCore:master into master
Reviewed-on: morrning/hesabixCore#1
2025-07-23 01:00:28 +03:30
367 changed files with 389857 additions and 6469 deletions

10
.env.local.php Normal file
View file

@ -0,0 +1,10 @@
<?php
return [
'APP_ENV' => 'prod',
'APP_SECRET' => 'f56179673fa562596e7fc565778a60f1',
'MESSENGER_TRANSPORT_DSN' => 'doctrine://default?auto_setup=0',
'MAILER_DSN' => 'null://null',
'CORS_ALLOW_ORIGIN' => '*',
'LOCK_DSN' => 'flock',
'DATABASE_URL' => 'mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4',
];

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" />

119
docs/DUPLICATE_CODE_FIX.md Normal file
View file

@ -0,0 +1,119 @@
# رفع مشکل کدهای تکراری حسابداری
## مشکل
سیستم حسابداری گاهی اوقات کدهای تکراری به اسناد می‌داد که باعث خطا در سیستم می‌شد. این مشکل به دلایل زیر رخ می‌داد:
1. **عدم بررسی تکراری بودن کد**: متد `getAccountingCode` کد جدیدی تولید می‌کرد بدون بررسی تکراری بودن
2. **Race Condition**: در صورت ارسال چندین درخواست همزمان، کدهای یکسانی تولید می‌شد
3. **عدم استفاده از تراکنش**: عملیات بدون تراکنش انجام می‌شد
## راه‌حل‌های پیاده‌سازی شده
### 1. بهبود متد `getAccountingCode` در `Provider.php`
#### تغییرات:
- **استفاده از تراکنش دیتابیس**: برای جلوگیری از Race Condition
- **بررسی تکراری بودن کد**: قبل از ذخیره، بررسی می‌شود که کد قبلاً وجود نداشته باشد
- **Retry Logic**: در صورت تکراری بودن، تا 10 بار تلاش می‌کند
- **Timestamp Fallback**: در صورت عدم موفقیت، از timestamp استفاده می‌کند
#### کد جدید:
```php
public function getAccountingCode($bid, $part)
{
$maxRetries = 10;
$retryCount = 0;
do {
$retryCount++;
$this->entityManager->beginTransaction();
try {
// تولید کد جدید
$newCode = intval($count) + 1;
// بررسی تکراری بودن
$isDuplicate = $this->checkCodeDuplicate($bid, $part, $newCode);
if (!$isDuplicate) {
// کد منحصر به فرد است
$business->{$setter}($newCode);
$this->entityManager->persist($business);
$this->entityManager->flush();
$this->entityManager->commit();
return $newCode;
} else {
// کد تکراری است، دوباره تلاش کن
if ($retryCount >= $maxRetries) {
$timestampCode = $this->generateTimestampCode($bid, $part);
return $timestampCode;
}
}
} catch (\Exception $e) {
$this->entityManager->rollback();
throw $e;
}
} while (true);
}
```
### 2. بهبود متد `app_accounting_insert` در `HesabdariController.php`
#### تغییرات:
- **استفاده از تراکنش**: کل عملیات در یک تراکنش انجام می‌شود
- **مدیریت خطا**: در صورت بروز خطا، تراکنش rollback می‌شود
- **بررسی خطای تولید کد**: خطاهای احتمالی در تولید کد مدیریت می‌شود
### 3. متدهای کمکی جدید
#### `checkCodeDuplicate()`:
بررسی تکراری بودن کد در جدول مربوطه
#### `generateTimestampCode()`:
تولید کد منحصر به فرد با استفاده از timestamp
#### `fixDuplicateCodes()`:
ترمیم کدهای تکراری موجود در دیتابیس
### 4. API جدید برای ترمیم کدهای تکراری
#### Endpoint: `/api/accounting/fix-duplicate-codes`
- **Method**: POST
- **Access**: فقط ادمین
- **Function**: ترمیم کدهای تکراری موجود
## نحوه استفاده
### برای ترمیم کدهای تکراری موجود:
```bash
POST /api/accounting/fix-duplicate-codes
Content-Type: application/json
{
"part": "accounting"
}
```
### پاسخ:
```json
{
"result": 1,
"message": "5 کد تکراری ترمیم شد",
"fixed_count": 5
}
```
## مزایای راه‌حل
1. **جلوگیری از کدهای تکراری**: سیستم اکنون کدهای منحصر به فرد تولید می‌کند
2. **مدیریت Race Condition**: استفاده از تراکنش از تداخل عملیات جلوگیری می‌کند
3. **ترمیم خودکار**: در صورت بروز مشکل، سیستم خودکار کد جدید تولید می‌کند
4. **ابزار ترمیم**: امکان ترمیم کدهای تکراری موجود
5. **Backward Compatibility**: تغییرات بدون تأثیر بر عملکرد موجود
## نکات مهم
1. **فقط ادمین**: فقط کاربران ادمین می‌توانند کدهای تکراری را ترمیم کنند
2. **Backup**: قبل از اجرای ترمیم، از دیتابیس backup بگیرید
3. **تست**: تغییرات را در محیط تست بررسی کنید
4. **Monitoring**: عملکرد سیستم را پس از اعمال تغییرات نظارت کنید

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,146 @@
# رفع خطای NonUniqueResultException در کدهای تکراری
## مشکل
خطای `NonUniqueResultException` با پیام "More than one result was found for query although one row or none was expected" در متد `app_accounting_remove_doc` رخ می‌دهد.
## علت
این خطا زمانی رخ می‌دهد که چندین سند حسابداری با کد یکسان در دیتابیس وجود دارد و متد `findOneBy` نمی‌تواند تصمیم بگیرد کدام سند را برگرداند.
## راه‌حل‌های پیاده‌سازی شده
### 1. بهبود متد `app_accounting_remove_doc`
#### تغییرات:
- **بررسی خودکار کدهای تکراری**: قبل از حذف سند، بررسی می‌شود که آیا کدهای تکراری وجود دارد
- **ترمیم خودکار**: در صورت وجود کدهای تکراری، سیستم خودکار آن‌ها را ترمیم می‌کند
- **پیدا کردن ایمن**: پس از ترمیم، سند به صورت ایمن پیدا می‌شود
#### کد جدید:
```php
// ابتدا بررسی کن که آیا کدهای تکراری وجود دارد
if ($provider->hasDuplicateCodes($request->headers->get('activeBid'), 'accounting')) {
// کدهای تکراری وجود دارد، ترمیم کن
$provider->fixDuplicateCodes($request->headers->get('activeBid'), 'accounting');
}
// حالا سند را پیدا کن
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'code' => $params['code'],
'bid' => $request->headers->get('activeBid')
]);
```
### 2. متد جدید `hasDuplicateCodes`
#### عملکرد:
- بررسی وجود کدهای تکراری بدون ترمیم
- بازگشت `true` یا `false`
- قابل استفاده برای بررسی وضعیت قبل از عملیات
#### کد:
```php
public function hasDuplicateCodes($bid, $part = 'accounting')
{
// پیدا کردن کدهای تکراری
$qb = $repository->createQueryBuilder('e');
$qb->select('e.code, COUNT(e.id) as count')
->where('e.bid = :bid')
->setParameter('bid', $bid)
->groupBy('e.code')
->having('COUNT(e.id) > 1');
$duplicates = $qb->getQuery()->getResult();
return count($duplicates) > 0;
}
```
### 3. API جدید برای بررسی وضعیت
#### Endpoint: `/api/accounting/check-duplicate-codes`
- **Method**: GET
- **Access**: فقط ادمین
- **Function**: بررسی وجود کدهای تکراری
#### مثال استفاده:
```bash
GET /api/accounting/check-duplicate-codes?part=accounting
```
#### پاسخ:
```json
{
"result": 1,
"has_duplicates": true,
"message": "کدهای تکراری یافت شد"
}
```
## نحوه استفاده
### 1. بررسی وضعیت کدهای تکراری:
```bash
GET /api/accounting/check-duplicate-codes?part=accounting
```
### 2. ترمیم کدهای تکراری:
```bash
POST /api/accounting/fix-duplicate-codes
Content-Type: application/json
{
"part": "accounting"
}
```
### 3. حذف سند (خودکار ترمیم می‌کند):
```bash
POST /api/accounting/remove
Content-Type: application/json
{
"code": "1001"
}
```
## مزایای راه‌حل
1. **جلوگیری از خطا**: خطای `NonUniqueResultException` دیگر رخ نمی‌دهد
2. **ترمیم خودکار**: سیستم خودکار کدهای تکراری را ترمیم می‌کند
3. **بررسی وضعیت**: امکان بررسی وجود کدهای تکراری قبل از عملیات
4. **Backward Compatibility**: با کدهای موجود سازگار است
5. **امنیت**: فقط ادمین می‌تواند عملیات ترمیم را انجام دهد
## نکات مهم
1. **عملکرد خودکار**: حذف سند اکنون خودکار کدهای تکراری را ترمیم می‌کند
2. **بررسی قبل از عملیات**: می‌توانید وضعیت کدهای تکراری را بررسی کنید
3. **ترمیم انتخابی**: می‌توانید فقط کدهای تکراری را ترمیم کنید
4. **لاگ عملیات**: تمام عملیات ترمیم در لاگ ثبت می‌شود
5. **Backup**: قبل از ترمیم، از دیتابیس backup بگیرید
## تست
برای تست عملکرد:
1. **ایجاد کدهای تکراری** (در محیط تست):
```sql
UPDATE hesabdari_doc SET code = 1001 WHERE id IN (1, 2);
```
2. **بررسی وضعیت**:
```bash
GET /api/accounting/check-duplicate-codes
```
3. **حذف سند** (خودکار ترمیم می‌کند):
```bash
POST /api/accounting/remove
```
4. **بررسی مجدد**:
```bash
GET /api/accounting/check-duplicate-codes
```
این راه‌حل مشکل `NonUniqueResultException` را به طور کامل حل می‌کند و سیستم را در برابر کدهای تکراری محافظت می‌کند.

View file

@ -0,0 +1,107 @@
# راهنمای کدهای عددی حسابداری
## اصل کلی
**تمام کدهای اسناد حسابداری باید فقط عدد باشند و هیچ حرفی نداشته باشند.**
## تغییرات اعمال شده
### 1. اعتبارسنجی کد
متد `validateCode()` اضافه شد که موارد زیر را بررسی می‌کند:
- کد باید عدد باشد
- کد باید مثبت باشد (بزرگتر از صفر)
- طول کد حداکثر 20 رقم باشد
### 2. بهبود متد `getAccountingCode`
- **بررسی تکراری بودن**: قبل از ذخیره، بررسی می‌شود که کد قبلاً وجود نداشته باشد
- **Retry Logic**: تا 10 بار تلاش برای تولید کد منحصر به فرد
- **Fallback Strategy**: در صورت عدم موفقیت، از شمارنده بزرگتر استفاده می‌کند
- **Timestamp Fallback**: در نهایت از timestamp استفاده می‌کند
### 3. متدهای جدید
#### `generateFallbackCode()`
تولید کد جایگزین با افزایش شمارنده به مقدار بزرگی (10000+)
#### `generateTimestampCode()`
تولید کد منحصر به فرد با استفاده از timestamp (فقط عدد)
#### `validateCode()`
اعتبارسنجی کد برای اطمینان از عددی بودن
## نمونه کدهای تولید شده
### کدهای عادی (ترتیبی):
```
1001, 1002, 1003, 1004, ...
```
### کدهای Fallback (در صورت تداخل):
```
11001, 11002, 11003, ...
```
### کدهای Timestamp (در صورت نیاز):
```
170312345678901234, 170312345678901235, ...
```
## قوانین اعتبارسنجی
### ✅ کدهای معتبر:
- `123`
- `1000`
- `999999`
- `170312345678901234`
### ❌ کدهای نامعتبر:
- `abc`
- `123abc`
- `0`
- `-1`
- `123.45`
## تست‌ها
فایل `ProviderTest.php` اضافه شد که شامل:
- تست تولید کد عددی
- تست اعتبارسنجی کد
- تست کدهای معتبر و نامعتبر
## نحوه اجرای تست
```bash
# اجرای تست‌های Provider
php bin/phpunit tests/ProviderTest.php
# اجرای تست خاص
php bin/phpunit --filter testGetAccountingCodeReturnsNumericValue
```
## مزایای راه‌حل
1. **اطمینان از عددی بودن**: تمام کدها فقط عدد هستند
2. **جلوگیری از تداخل**: سیستم خودکار کد منحصر به فرد تولید می‌کند
3. **Backward Compatibility**: با کدهای موجود سازگار است
4. **قابلیت تست**: تست‌های کامل برای اطمینان از عملکرد صحیح
5. **مدیریت خطا**: در صورت بروز مشکل، راه‌حل جایگزین ارائه می‌دهد
## نکات مهم
1. **فقط عدد**: هیچ حرفی در کدها استفاده نمی‌شود
2. **مثبت**: تمام کدها بزرگتر از صفر هستند
3. **منحصر به فرد**: هیچ کد تکراری تولید نمی‌شود
4. **قابل پیش‌بینی**: کدها ترتیبی هستند (مگر در موارد خاص)
5. **قابل ترمیم**: ابزار ترمیم کدهای تکراری موجود
## API ترمیم کدهای تکراری
```bash
POST /api/accounting/fix-duplicate-codes
Content-Type: application/json
{
"part": "accounting"
}
```
این API کدهای تکراری موجود را پیدا کرده و با کدهای عددی منحصر به فرد جایگزین می‌کند.

View file

@ -0,0 +1,129 @@
# تولید کدهای معقول حسابداری
## مشکل
کدهای تولید شده خیلی بلند و خارج از عرف حسابداری بودند (مثل `1,756,382,866,131,764`).
## راه‌حل‌های پیاده‌سازی شده
### 1. محدودیت طول کد
- **حداکثر 6 رقم**: کدهای حسابداری حداکثر 6 رقم هستند
- **عرف حسابداری**: مطابق با استانداردهای حسابداری
### 2. متد `generateReasonableCode`
#### عملکرد:
- تولید کدهای 1 تا 6 رقمی
- استفاده از شمارنده ترتیبی
- استفاده از اعداد تصادفی در صورت نیاز
- بررسی تکراری نبودن
#### الگوریتم:
1. **شمارنده ترتیبی**: `currentCode + 1`
2. **عدد تصادفی 4 رقمی**: `1000-9999`
3. **عدد تصادفی 5 رقمی**: `10000-99999`
4. **عدد تصادفی 6 رقمی**: `100000-999999`
### 3. بهبود متدهای موجود
#### `generateTimestampCode`:
- حذف timestamp های بلند
- استفاده از شمارنده معقول
- حداکثر 6 رقم
#### `generateFallbackCode`:
- کاهش افزایش شمارنده از 10000 به 500
- استفاده از اعداد تصادفی معقول
#### `validateCode`:
- محدودیت طول از 20 رقم به 6 رقم
## نمونه کدهای تولید شده
### ✅ کدهای معقول:
```
1001, 1002, 1003, ... (ترتیبی)
1234, 5678, 9012, ... (تصادفی 4 رقمی)
12345, 67890, ... (تصادفی 5 رقمی)
123456, 789012, ... (تصادفی 6 رقمی)
```
### ❌ کدهای نامعتبر (قبلی):
```
1,756,382,866,131,764 (خیلی بلند)
170312345678901234 (timestamp بلند)
```
## مزایای راه‌حل
1. **مطابق عرف**: کدهای 1-6 رقمی مطابق استاندارد حسابداری
2. **قابل خواندن**: کدهای کوتاه و قابل فهم
3. **عملکرد بهتر**: تولید سریع‌تر کدها
4. **ذخیره‌سازی بهینه**: فضای کمتری در دیتابیس
5. **نمایش بهتر**: در گزارش‌ها و فاکتورها بهتر نمایش داده می‌شوند
## نحوه استفاده
### تولید کد جدید:
```php
$code = $provider->getAccountingCode($bid, 'accounting');
// نتیجه: 1001, 1002, 1234, 12345, 123456
```
### ترمیم کدهای تکراری:
```php
$result = $provider->fixDuplicateCodes($bid, 'accounting');
// کدهای تکراری با کدهای معقول جایگزین می‌شوند
```
## قوانین جدید
### اعتبارسنجی کد:
- ✅ عدد مثبت
- ✅ حداکثر 6 رقم
- ✅ فقط اعداد
### تولید کد:
1. **اولویت اول**: شمارنده ترتیبی
2. **اولویت دوم**: عدد تصادفی 4 رقمی
3. **اولویت سوم**: عدد تصادفی 5 رقمی
4. **اولویت چهارم**: عدد تصادفی 6 رقمی
## تست
### تست کدهای معقول:
```php
// تست تولید کد
$code = $provider->getAccountingCode($bid, 'accounting');
$this->assertLessThanOrEqual(999999, $code); // حداکثر 6 رقم
$this->assertGreaterThan(0, $code); // مثبت
// تست اعتبارسنجی
$this->assertTrue($provider->validateCode(1234)); // معتبر
$this->assertFalse($provider->validateCode(1234567)); // خیلی بلند
```
## نکات مهم
1. **عرف حسابداری**: کدهای 1-6 رقمی استاندارد هستند
2. **عملکرد**: تولید سریع‌تر کدها
3. **خوانایی**: کدهای کوتاه قابل فهم‌تر
4. **نمایش**: در گزارش‌ها بهتر نمایش داده می‌شوند
5. **Backward Compatibility**: با کدهای موجود سازگار
## تغییرات در API
### قبل:
```json
{
"code": "1756382866131764"
}
```
### بعد:
```json
{
"code": "1234"
}
```
این تغییرات کدهای حسابداری را مطابق با عرف و استانداردهای حسابداری می‌کند.

View file

@ -0,0 +1,155 @@
# پیدا کردن ایمن Entity ها با بررسی کدهای تکراری
## مشکل
خطای `NonUniqueResultException` در متدهای مختلف که از `findOneBy` استفاده می‌کنند رخ می‌دهد.
## راه‌حل‌های پیاده‌سازی شده
### 1. متد `findHesabdariDocSafely`
#### عملکرد:
- بررسی خودکار کدهای تکراری قبل از پیدا کردن سند
- ترمیم خودکار کدهای تکراری در صورت نیاز
- پیدا کردن ایمن سند حسابداری
#### استفاده:
```php
$doc = $provider->findHesabdariDocSafely($entityManager, [
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['code'],
'money' => $acc['money']
]);
```
### 2. متد `findEntitySafely` (عمومی)
#### عملکرد:
- بررسی خودکار کدهای تکراری برای هر نوع entity
- ترمیم خودکار در صورت نیاز
- قابل استفاده برای تمام entity ها
#### استفاده:
```php
// برای سند حسابداری
$doc = $provider->findEntitySafely($entityManager, HesabdariDoc::class, [
'bid' => $acc['bid'],
'code' => $params['code']
], 'accounting');
// برای شخص
$person = $provider->findEntitySafely($entityManager, Person::class, [
'bid' => $acc['bid'],
'code' => $params['code']
], 'person');
// برای حساب بانکی
$bank = $provider->findEntitySafely($entityManager, BankAccount::class, [
'bid' => $acc['bid'],
'code' => $params['code']
], 'bank');
```
## متدهای بهبود یافته
### 1. `app_accounting_doc_get`
```php
// قبل از بهبود
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['code'],
'money' => $acc['money']
]);
// بعد از بهبود
$doc = $provider->findHesabdariDocSafely($entityManager, [
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['code'],
'money' => $acc['money']
]);
```
### 2. `app_accounting_remove_doc`
```php
// قبل از بهبود
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'code' => $params['code'],
'bid' => $request->headers->get('activeBid')
]);
// بعد از بهبود
$doc = $provider->findHesabdariDocSafely($entityManager, [
'code' => $params['code'],
'bid' => $request->headers->get('activeBid')
]);
```
## مزایای راه‌حل
1. **جلوگیری از خطا**: خطای `NonUniqueResultException` دیگر رخ نمی‌دهد
2. **ترمیم خودکار**: کدهای تکراری خودکار ترمیم می‌شوند
3. **قابلیت استفاده مجدد**: متدهای عمومی قابل استفاده در تمام بخش‌ها
4. **Backward Compatibility**: با کدهای موجود سازگار است
5. **عملکرد بهینه**: فقط در صورت نیاز ترمیم انجام می‌شود
## نحوه استفاده در سایر کنترلرها
### برای کنترلر Person:
```php
// قبل
$person = $entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['code']
]);
// بعد
$person = $provider->findEntitySafely($entityManager, Person::class, [
'bid' => $acc['bid'],
'code' => $params['code']
], 'person');
```
### برای کنترلر Bank:
```php
// قبل
$bank = $entityManager->getRepository(BankAccount::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['code']
]);
// بعد
$bank = $provider->findEntitySafely($entityManager, BankAccount::class, [
'bid' => $acc['bid'],
'code' => $params['code']
], 'bank');
```
## نکات مهم
1. **پارامتر part**: برای entity هایی که کد دارند، part را مشخص کنید
2. **عملکرد خودکار**: ترمیم فقط در صورت وجود کدهای تکراری انجام می‌شود
3. **امنیت**: تمام عملیات در تراکنش انجام می‌شود
4. **لاگ**: عملیات ترمیم در لاگ ثبت می‌شود
5. **Backup**: قبل از استفاده، از دیتابیس backup بگیرید
## تست
برای تست عملکرد:
```php
// تست پیدا کردن ایمن
$doc = $provider->findHesabdariDocSafely($entityManager, [
'bid' => $bid,
'code' => '1001'
]);
// تست پیدا کردن عمومی
$person = $provider->findEntitySafely($entityManager, Person::class, [
'bid' => $bid,
'code' => 'P001'
], 'person');
```
این راه‌حل مشکل `NonUniqueResultException` را در تمام متدها حل می‌کند و سیستم را در برابر کدهای تکراری محافظت می‌کند.

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

@ -3,8 +3,8 @@
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Generation Time: Mar 21, 2025 at 08:34 PM
-- Server version: 8.0.41-0ubuntu0.24.04.1
-- Generation Time: Jul 23, 2025 at 02:10 PM
-- Server version: 8.0.42-0ubuntu0.24.04.2
-- PHP Version: 8.3.6
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
@ -18,7 +18,7 @@ SET time_zone = "+00:00";
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `hesabix`
-- Database: `hesabix_next_hesabix_ir`
--
-- --------------------------------------------------------
@ -45,6 +45,58 @@ CREATE TABLE `accounting_package_order` (
-- --------------------------------------------------------
--
-- Table structure for table `account_to_sheba_inquiry`
--
CREATE TABLE `account_to_sheba_inquiry` (
`id` int NOT NULL,
`cache_key` varchar(50) NOT NULL,
`sheba_data` json NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `aiconversation`
--
CREATE TABLE `aiconversation` (
`id` int NOT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`category` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` int NOT NULL,
`updated_at` int NOT NULL,
`is_active` tinyint(1) DEFAULT NULL,
`deleted` tinyint(1) DEFAULT NULL,
`user_id` int NOT NULL,
`business_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `aimessage`
--
CREATE TABLE `aimessage` (
`id` int NOT NULL,
`role` varchar(20) NOT NULL,
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` int NOT NULL,
`input_tokens` int DEFAULT NULL,
`output_tokens` int DEFAULT NULL,
`input_cost` double DEFAULT NULL,
`output_cost` double DEFAULT NULL,
`total_cost` double DEFAULT NULL,
`model` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`agent_source` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`conversation_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `apitoken`
--
@ -54,7 +106,8 @@ CREATE TABLE `apitoken` (
`bid_id` int DEFAULT NULL,
`submitter_id` int NOT NULL,
`token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`date_expire` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL
`date_expire` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`is_for_ai` tinyint(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
@ -104,6 +157,22 @@ CREATE TABLE `archive_orders` (
-- --------------------------------------------------------
--
-- Table structure for table `back_built_module`
--
CREATE TABLE `back_built_module` (
`id` int NOT NULL,
`date_submit` varchar(40) NOT NULL,
`code` longtext,
`locked` tinyint(1) DEFAULT NULL,
`public` tinyint(1) DEFAULT NULL,
`type` varchar(120) NOT NULL,
`submitter_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `bank_account`
--
@ -191,6 +260,19 @@ CREATE TABLE `business_money` (
-- --------------------------------------------------------
--
-- Table structure for table `card_to_sheba_inquiry`
--
CREATE TABLE `card_to_sheba_inquiry` (
`id` int NOT NULL,
`card_number` varchar(16) NOT NULL,
`sheba_data` json NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `cashdesk`
--
@ -243,7 +325,10 @@ CREATE TABLE `cheque` (
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`locked` tinyint(1) DEFAULT NULL,
`date` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`rejected` tinyint(1) DEFAULT NULL
`rejected` tinyint(1) DEFAULT NULL,
`transfered` tinyint(1) DEFAULT NULL,
`transfer_date` varchar(25) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`money_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- --------------------------------------------------------
@ -430,19 +515,22 @@ CREATE TABLE `hesabdari_doc` (
`date_submit` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`date` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`code` bigint NOT NULL,
`code` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`des` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`amount` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`amount` decimal(30,0) DEFAULT NULL,
`mdate` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`plugin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`ref_data` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`shortlink` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`wallet_transaction_id` int DEFAULT NULL,
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`temp_status` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '(DC2Type:array)',
`temp_status` json DEFAULT NULL,
`invoice_label_id` int DEFAULT NULL,
`project_id` int DEFAULT NULL,
`salesman_id` int DEFAULT NULL
`salesman_id` int DEFAULT NULL,
`tax_percent` double DEFAULT NULL,
`discount_type` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`discount_percent` decimal(10,2) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
@ -474,16 +562,18 @@ CREATE TABLE `hesabdari_row` (
`bid_id` int NOT NULL,
`year_id` int NOT NULL,
`commodity_id` int DEFAULT NULL,
`commdity_count` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`commdity_count` decimal(20,4) DEFAULT NULL,
`salary_id` int DEFAULT NULL,
`cashdesk_id` int DEFAULT NULL,
`referral` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`ref_data` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`plugin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`temp_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '(DC2Type:array)',
`temp_data` json DEFAULT NULL,
`cheque_id` int DEFAULT NULL,
`discount` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`tax` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL
`tax` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`discount_type` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL,
`discount_percent` decimal(10,2) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
@ -702,9 +792,9 @@ CREATE TABLE `messenger_messages` (
`body` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`headers` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`queue_name` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`created_at` datetime NOT NULL COMMENT '(DC2Type:datetime_immutable)',
`available_at` datetime NOT NULL COMMENT '(DC2Type:datetime_immutable)',
`delivered_at` datetime DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)'
`created_at` datetime NOT NULL,
`available_at` datetime NOT NULL,
`delivered_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
@ -851,7 +941,13 @@ CREATE TABLE `permission` (
`plug_accpro_rfsell` tinyint(1) DEFAULT NULL,
`plug_accpro_accounting` tinyint(1) DEFAULT NULL,
`plug_accpro_close_year` tinyint(1) DEFAULT NULL,
`plug_repservice` tinyint(1) DEFAULT NULL
`plug_repservice` tinyint(1) DEFAULT NULL,
`plug_accpro_presell` tinyint(1) DEFAULT NULL,
`plug_hrm_docs` tinyint(1) DEFAULT NULL,
`plug_ghesta_manager` tinyint(1) DEFAULT NULL,
`plug_tax_settings` tinyint(1) DEFAULT NULL,
`inquiry` tinyint(1) DEFAULT NULL,
`ai` tinyint(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
@ -1015,6 +1111,117 @@ INSERT INTO `plugin_prodect` (`id`, `name`, `code`, `timestamp`, `timelabel`, `p
-- --------------------------------------------------------
--
-- Table structure for table `plugin_taxsettings_keys`
--
CREATE TABLE `plugin_taxsettings_keys` (
`id` int NOT NULL,
`business_id` int NOT NULL,
`user_id` int NOT NULL,
`private_key` longtext,
`tax_memory_id` varchar(64) DEFAULT NULL,
`economic_code` varchar(64) DEFAULT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `plugin_tax_invoice`
--
CREATE TABLE `plugin_tax_invoice` (
`id` int NOT NULL,
`invoice_code` varchar(255) NOT NULL,
`tax_system_invoice_number` varchar(255) DEFAULT NULL,
`tax_system_reference_number` varchar(255) DEFAULT NULL,
`status` varchar(255) NOT NULL,
`response_data` longtext,
`error_message` longtext,
`created_at` datetime NOT NULL,
`sent_at` datetime DEFAULT NULL,
`confirmed_at` datetime DEFAULT NULL,
`amount` decimal(30,0) NOT NULL,
`customer_name` varchar(255) DEFAULT NULL,
`customer_id` varchar(255) DEFAULT NULL,
`invoice_type` varchar(50) DEFAULT NULL,
`business_id` int NOT NULL,
`user_id` int NOT NULL,
`invoice_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `plug_ghesta_doc`
--
CREATE TABLE `plug_ghesta_doc` (
`id` int NOT NULL,
`date_submit` varchar(25) NOT NULL,
`count` bigint NOT NULL,
`profit_percent` bigint NOT NULL,
`profit_amount` varchar(255) DEFAULT NULL,
`profit_type` varchar(30) DEFAULT NULL,
`days_pay` double DEFAULT NULL,
`bid_id` int NOT NULL,
`submitter_id` int DEFAULT NULL,
`person_id` int NOT NULL,
`main_doc_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `plug_ghesta_item`
--
CREATE TABLE `plug_ghesta_item` (
`id` int NOT NULL,
`date` varchar(25) NOT NULL,
`amount` varchar(120) NOT NULL,
`num` int NOT NULL,
`doc_id` int NOT NULL,
`hesabdari_doc_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `plug_hrm_doc`
--
CREATE TABLE `plug_hrm_doc` (
`id` int NOT NULL,
`description` varchar(255) NOT NULL,
`date` varchar(10) NOT NULL,
`create_date` int NOT NULL,
`business_id` int NOT NULL,
`creator_id` int NOT NULL,
`hesabdari_doc_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `plug_hrm_doc_item`
--
CREATE TABLE `plug_hrm_doc_item` (
`id` int NOT NULL,
`base_salary` int NOT NULL,
`overtime` int NOT NULL,
`shift` int NOT NULL,
`night` int NOT NULL,
`description` varchar(255) DEFAULT NULL,
`doc_id` int NOT NULL,
`person_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `plug_noghre_order`
--
@ -1092,6 +1299,20 @@ INSERT INTO `plug_repservice_order_state` (`id`, `label`, `code`) VALUES
-- --------------------------------------------------------
--
-- Table structure for table `postal_code_inquiry`
--
CREATE TABLE `postal_code_inquiry` (
`id` int NOT NULL,
`postal_code` varchar(10) NOT NULL,
`address_data` json NOT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `pre_invoice_doc`
--
@ -1113,7 +1334,13 @@ CREATE TABLE `pre_invoice_doc` (
`plugin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`ref_data` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`shortlink` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`tax_percent` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`total_discount` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`total_discount_percent` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`shipping_cost` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`show_percent_discount` tinyint(1) DEFAULT NULL,
`show_total_percent_discount` tinyint(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- --------------------------------------------------------
@ -1125,19 +1352,16 @@ CREATE TABLE `pre_invoice_doc` (
CREATE TABLE `pre_invoice_item` (
`id` int NOT NULL,
`commodity_id` int NOT NULL,
`person_id` int DEFAULT NULL,
`bank_id` int DEFAULT NULL,
`cashdesk_id` int DEFAULT NULL,
`salary_id` int DEFAULT NULL,
`bid_id` int NOT NULL,
`year_id` int NOT NULL,
`ref_id_id` int NOT NULL,
`commodity_count` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bs` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bd` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`des` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`discount` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`tax` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL
`tax` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`discount_percent` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`discount_amount` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`show_percent_discount` tinyint(1) DEFAULT NULL,
`doc_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- --------------------------------------------------------
@ -1254,7 +1478,11 @@ CREATE TABLE `print_options` (
`repservice_paper` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`fastsell_invoice` tinyint(1) DEFAULT NULL,
`fastsell_pdf` tinyint(1) DEFAULT NULL,
`fastsell_cashdesk_ticket` tinyint(1) DEFAULT NULL
`fastsell_cashdesk_ticket` tinyint(1) DEFAULT NULL,
`left_footer` longtext COLLATE utf8mb4_unicode_ci,
`right_footer` longtext COLLATE utf8mb4_unicode_ci,
`sell_invoice_index` tinyint(1) DEFAULT NULL,
`sell_business_stamp` tinyint(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- --------------------------------------------------------
@ -1292,7 +1520,7 @@ CREATE TABLE `registry` (
`id` int NOT NULL,
`root` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`value_of_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL
`value_of_key` longtext COLLATE utf8mb4_unicode_ci
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
@ -1603,6 +1831,28 @@ ALTER TABLE `accounting_package_order`
ADD KEY `IDX_CAA1774D4D9866B8` (`bid_id`),
ADD KEY `IDX_CAA1774D919E5513` (`submitter_id`);
--
-- Indexes for table `account_to_sheba_inquiry`
--
ALTER TABLE `account_to_sheba_inquiry`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `UNIQ_3B99BD82763247D7` (`cache_key`);
--
-- Indexes for table `aiconversation`
--
ALTER TABLE `aiconversation`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_B85427A8A76ED395` (`user_id`),
ADD KEY `IDX_B85427A8A89DB457` (`business_id`);
--
-- Indexes for table `aimessage`
--
ALTER TABLE `aimessage`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_4964D5D89AC0396` (`conversation_id`);
--
-- Indexes for table `apitoken`
--
@ -1627,6 +1877,13 @@ ALTER TABLE `archive_orders`
ADD KEY `IDX_182AE9FB4D9866B8` (`bid_id`),
ADD KEY `IDX_182AE9FB919E5513` (`submitter_id`);
--
-- Indexes for table `back_built_module`
--
ALTER TABLE `back_built_module`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_DB9B83EB919E5513` (`submitter_id`);
--
-- Indexes for table `bank_account`
--
@ -1652,6 +1909,13 @@ ALTER TABLE `business_money`
ADD KEY `IDX_C93EF45BA89DB457` (`business_id`),
ADD KEY `IDX_C93EF45BBF29332C` (`money_id`);
--
-- Indexes for table `card_to_sheba_inquiry`
--
ALTER TABLE `card_to_sheba_inquiry`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `UNIQ_CE18D9D4E4AF4C20` (`card_number`);
--
-- Indexes for table `cashdesk`
--
@ -1675,7 +1939,8 @@ ALTER TABLE `cheque`
ADD KEY `IDX_A0BBFDE9919E5513` (`submitter_id`),
ADD KEY `IDX_A0BBFDE911C8FB41` (`bank_id`),
ADD KEY `IDX_A0BBFDE9217BBB47` (`person_id`),
ADD KEY `IDX_A0BBFDE921B741A9` (`ref_id`);
ADD KEY `IDX_A0BBFDE921B741A9` (`ref_id`),
ADD KEY `IDX_A0BBFDE9BF29332C` (`money_id`);
--
-- Indexes for table `commodity`
@ -1914,6 +2179,56 @@ ALTER TABLE `plugin`
ALTER TABLE `plugin_prodect`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `plugin_taxsettings_keys`
--
ALTER TABLE `plugin_taxsettings_keys`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `plugin_tax_invoice`
--
ALTER TABLE `plugin_tax_invoice`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_165525F4A89DB457` (`business_id`),
ADD KEY `IDX_165525F4A76ED395` (`user_id`),
ADD KEY `IDX_165525F42989F1FD` (`invoice_id`);
--
-- Indexes for table `plug_ghesta_doc`
--
ALTER TABLE `plug_ghesta_doc`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_2874D6C94D9866B8` (`bid_id`),
ADD KEY `IDX_2874D6C9919E5513` (`submitter_id`),
ADD KEY `IDX_2874D6C9217BBB47` (`person_id`),
ADD KEY `IDX_2874D6C9164AF0AA` (`main_doc_id`);
--
-- Indexes for table `plug_ghesta_item`
--
ALTER TABLE `plug_ghesta_item`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_B7D2CF60895648BC` (`doc_id`),
ADD KEY `IDX_B7D2CF6074826F51` (`hesabdari_doc_id`);
--
-- Indexes for table `plug_hrm_doc`
--
ALTER TABLE `plug_hrm_doc`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_D44A2689A89DB457` (`business_id`),
ADD KEY `IDX_D44A268961220EA6` (`creator_id`),
ADD KEY `IDX_D44A268974826F51` (`hesabdari_doc_id`);
--
-- Indexes for table `plug_hrm_doc_item`
--
ALTER TABLE `plug_hrm_doc_item`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_E3C87F09895648BC` (`doc_id`),
ADD KEY `IDX_E3C87F09217BBB47` (`person_id`);
--
-- Indexes for table `plug_noghre_order`
--
@ -1944,6 +2259,13 @@ ALTER TABLE `plug_repservice_order`
ALTER TABLE `plug_repservice_order_state`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `postal_code_inquiry`
--
ALTER TABLE `postal_code_inquiry`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `UNIQ_E5578D40EA98E376` (`postal_code`);
--
-- Indexes for table `pre_invoice_doc`
--
@ -1963,13 +2285,7 @@ ALTER TABLE `pre_invoice_doc`
ALTER TABLE `pre_invoice_item`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_DD881165B4ACC212` (`commodity_id`),
ADD KEY `IDX_DD881165217BBB47` (`person_id`),
ADD KEY `IDX_DD88116511C8FB41` (`bank_id`),
ADD KEY `IDX_DD881165BA216AA5` (`cashdesk_id`),
ADD KEY `IDX_DD881165B0FDF16E` (`salary_id`),
ADD KEY `IDX_DD8811654D9866B8` (`bid_id`),
ADD KEY `IDX_DD88116540C1FEA7` (`year_id`),
ADD KEY `IDX_DD881165C8FFB95` (`ref_id_id`);
ADD KEY `IDX_DD881165895648BC` (`doc_id`);
--
-- Indexes for table `price_list`
@ -2163,6 +2479,24 @@ ALTER TABLE `year`
ALTER TABLE `accounting_package_order`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=16;
--
-- AUTO_INCREMENT for table `account_to_sheba_inquiry`
--
ALTER TABLE `account_to_sheba_inquiry`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `aiconversation`
--
ALTER TABLE `aiconversation`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `aimessage`
--
ALTER TABLE `aimessage`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `apitoken`
--
@ -2181,6 +2515,12 @@ ALTER TABLE `archive_file`
ALTER TABLE `archive_orders`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `back_built_module`
--
ALTER TABLE `back_built_module`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `bank_account`
--
@ -2193,6 +2533,12 @@ ALTER TABLE `bank_account`
ALTER TABLE `business`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT for table `card_to_sheba_inquiry`
--
ALTER TABLE `card_to_sheba_inquiry`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `cashdesk`
--
@ -2367,6 +2713,42 @@ ALTER TABLE `plugin`
ALTER TABLE `plugin_prodect`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7;
--
-- AUTO_INCREMENT for table `plugin_taxsettings_keys`
--
ALTER TABLE `plugin_taxsettings_keys`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `plugin_tax_invoice`
--
ALTER TABLE `plugin_tax_invoice`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `plug_ghesta_doc`
--
ALTER TABLE `plug_ghesta_doc`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `plug_ghesta_item`
--
ALTER TABLE `plug_ghesta_item`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `plug_hrm_doc`
--
ALTER TABLE `plug_hrm_doc`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `plug_hrm_doc_item`
--
ALTER TABLE `plug_hrm_doc_item`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `plug_noghre_order`
--
@ -2385,6 +2767,12 @@ ALTER TABLE `plug_repservice_order`
ALTER TABLE `plug_repservice_order_state`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7;
--
-- AUTO_INCREMENT for table `postal_code_inquiry`
--
ALTER TABLE `postal_code_inquiry`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `pre_invoice_doc`
--
@ -2552,6 +2940,19 @@ ALTER TABLE `accounting_package_order`
ADD CONSTRAINT `FK_CAA1774D4D9866B8` FOREIGN KEY (`bid_id`) REFERENCES `business` (`id`),
ADD CONSTRAINT `FK_CAA1774D919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`);
--
-- Constraints for table `aiconversation`
--
ALTER TABLE `aiconversation`
ADD CONSTRAINT `FK_B85427A8A76ED395` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
ADD CONSTRAINT `FK_B85427A8A89DB457` FOREIGN KEY (`business_id`) REFERENCES `business` (`id`);
--
-- Constraints for table `aimessage`
--
ALTER TABLE `aimessage`
ADD CONSTRAINT `FK_4964D5D89AC0396` FOREIGN KEY (`conversation_id`) REFERENCES `aiconversation` (`id`);
--
-- Constraints for table `apitoken`
--
@ -2573,6 +2974,12 @@ ALTER TABLE `archive_orders`
ADD CONSTRAINT `FK_182AE9FB4D9866B8` FOREIGN KEY (`bid_id`) REFERENCES `business` (`id`),
ADD CONSTRAINT `FK_182AE9FB919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`);
--
-- Constraints for table `back_built_module`
--
ALTER TABLE `back_built_module`
ADD CONSTRAINT `FK_DB9B83EB919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`);
--
-- Constraints for table `bank_account`
--
@ -2610,7 +3017,8 @@ ALTER TABLE `cheque`
ADD CONSTRAINT `FK_A0BBFDE9217BBB47` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`),
ADD CONSTRAINT `FK_A0BBFDE921B741A9` FOREIGN KEY (`ref_id`) REFERENCES `hesabdari_table` (`id`),
ADD CONSTRAINT `FK_A0BBFDE94D9866B8` FOREIGN KEY (`bid_id`) REFERENCES `business` (`id`),
ADD CONSTRAINT `FK_A0BBFDE9919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`);
ADD CONSTRAINT `FK_A0BBFDE9919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`),
ADD CONSTRAINT `FK_A0BBFDE9BF29332C` FOREIGN KEY (`money_id`) REFERENCES `money` (`id`);
--
-- Constraints for table `commodity`
@ -2669,8 +3077,8 @@ ALTER TABLE `hesabdari_doc`
-- Constraints for table `hesabdari_doc_hesabdari_doc`
--
ALTER TABLE `hesabdari_doc_hesabdari_doc`
ADD CONSTRAINT `FK_BE675746E2A225E5` FOREIGN KEY (`hesabdari_doc_source`) REFERENCES `hesabdari_doc` (`id`) ON DELETE CASCADE,
ADD CONSTRAINT `FK_BE675746FB47756A` FOREIGN KEY (`hesabdari_doc_target`) REFERENCES `hesabdari_doc` (`id`) ON DELETE CASCADE;
ADD CONSTRAINT `FK_BE675746E2A225E5` FOREIGN KEY (`hesabdari_doc_source`) REFERENCES `hesabdari_doc` (`id`),
ADD CONSTRAINT `FK_BE675746FB47756A` FOREIGN KEY (`hesabdari_doc_target`) REFERENCES `hesabdari_doc` (`id`);
--
-- Constraints for table `hesabdari_row`
@ -2781,6 +3189,45 @@ ALTER TABLE `plugin`
ADD CONSTRAINT `FK_E96E27944D9866B8` FOREIGN KEY (`bid_id`) REFERENCES `business` (`id`),
ADD CONSTRAINT `FK_E96E2794919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`);
--
-- Constraints for table `plugin_tax_invoice`
--
ALTER TABLE `plugin_tax_invoice`
ADD CONSTRAINT `FK_165525F42989F1FD` FOREIGN KEY (`invoice_id`) REFERENCES `hesabdari_doc` (`id`),
ADD CONSTRAINT `FK_165525F4A76ED395` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
ADD CONSTRAINT `FK_165525F4A89DB457` FOREIGN KEY (`business_id`) REFERENCES `business` (`id`);
--
-- Constraints for table `plug_ghesta_doc`
--
ALTER TABLE `plug_ghesta_doc`
ADD CONSTRAINT `FK_2874D6C9164AF0AA` FOREIGN KEY (`main_doc_id`) REFERENCES `hesabdari_doc` (`id`),
ADD CONSTRAINT `FK_2874D6C9217BBB47` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`),
ADD CONSTRAINT `FK_2874D6C94D9866B8` FOREIGN KEY (`bid_id`) REFERENCES `business` (`id`),
ADD CONSTRAINT `FK_2874D6C9919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`);
--
-- Constraints for table `plug_ghesta_item`
--
ALTER TABLE `plug_ghesta_item`
ADD CONSTRAINT `FK_B7D2CF6074826F51` FOREIGN KEY (`hesabdari_doc_id`) REFERENCES `hesabdari_doc` (`id`),
ADD CONSTRAINT `FK_B7D2CF60895648BC` FOREIGN KEY (`doc_id`) REFERENCES `plug_ghesta_doc` (`id`);
--
-- Constraints for table `plug_hrm_doc`
--
ALTER TABLE `plug_hrm_doc`
ADD CONSTRAINT `FK_D44A268961220EA6` FOREIGN KEY (`creator_id`) REFERENCES `user` (`id`),
ADD CONSTRAINT `FK_D44A268974826F51` FOREIGN KEY (`hesabdari_doc_id`) REFERENCES `hesabdari_doc` (`id`),
ADD CONSTRAINT `FK_D44A2689A89DB457` FOREIGN KEY (`business_id`) REFERENCES `business` (`id`);
--
-- Constraints for table `plug_hrm_doc_item`
--
ALTER TABLE `plug_hrm_doc_item`
ADD CONSTRAINT `FK_E3C87F09217BBB47` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`),
ADD CONSTRAINT `FK_E3C87F09895648BC` FOREIGN KEY (`doc_id`) REFERENCES `plug_hrm_doc` (`id`);
--
-- Constraints for table `plug_noghre_order`
--
@ -2819,14 +3266,8 @@ ALTER TABLE `pre_invoice_doc`
-- Constraints for table `pre_invoice_item`
--
ALTER TABLE `pre_invoice_item`
ADD CONSTRAINT `FK_DD88116511C8FB41` FOREIGN KEY (`bank_id`) REFERENCES `bank_account` (`id`),
ADD CONSTRAINT `FK_DD881165217BBB47` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`),
ADD CONSTRAINT `FK_DD88116540C1FEA7` FOREIGN KEY (`year_id`) REFERENCES `year` (`id`),
ADD CONSTRAINT `FK_DD8811654D9866B8` FOREIGN KEY (`bid_id`) REFERENCES `business` (`id`),
ADD CONSTRAINT `FK_DD881165B0FDF16E` FOREIGN KEY (`salary_id`) REFERENCES `salary` (`id`),
ADD CONSTRAINT `FK_DD881165B4ACC212` FOREIGN KEY (`commodity_id`) REFERENCES `commodity` (`id`),
ADD CONSTRAINT `FK_DD881165BA216AA5` FOREIGN KEY (`cashdesk_id`) REFERENCES `cashdesk` (`id`),
ADD CONSTRAINT `FK_DD881165C8FFB95` FOREIGN KEY (`ref_id_id`) REFERENCES `hesabdari_table` (`id`);
ADD CONSTRAINT `FK_DD881165895648BC` FOREIGN KEY (`doc_id`) REFERENCES `pre_invoice_doc` (`id`),
ADD CONSTRAINT `FK_DD881165B4ACC212` FOREIGN KEY (`commodity_id`) REFERENCES `commodity` (`id`);
--
-- Constraints for table `price_list`

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

@ -10,10 +10,10 @@ framework:
session:
enabled: true
handler_id: null
cookie_secure: true
cookie_secure: false
storage_factory_id: session.storage.factory.native
cookie_lifetime: 3600 # 1 ساعت
cookie_samesite: none # برای CORS باید Lax یا None باشه
cookie_samesite: lax # برای CORS باید Lax یا None باشه
save_path: '%kernel.project_dir%/var/sessions'
gc_maxlifetime: 3600
gc_probability: 1
@ -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

@ -8,7 +8,6 @@ security:
app_user_provider:
entity:
class: App\Entity\User
property: mobile
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
@ -29,6 +28,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 +48,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

@ -4,6 +4,22 @@ parameters:
avatarDir: '%kernel.project_dir%/../hesabixArchive/avatars'
sealDir: '%kernel.project_dir%/../hesabixArchive/seal'
SupportFilesDir: '%kernel.project_dir%/../hesabixArchive/support'
# تنظیمات سیستم بستن سال مالی
close_year.accounts.profit_loss: '999999'
close_year.accounts.retained_earnings: '999998'
close_year.account_types.temporary: ['calc'] # حساب‌های موقت (درآمد و هزینه)
close_year.account_types.permanent: ['calc'] # حساب‌های دائمی (دارایی، بدهی، سرمایه)
close_year.defaults.tax_percent: 0
close_year.defaults.dividend_percent: 0
close_year.defaults.new_year_duration: 31563000
close_year.backup.enabled: true
close_year.backup.directory: '%kernel.project_dir%/var/backups/'
close_year.logging.enabled: true
close_year.logging.level: 'info'
close_year.security.required_role: 'plugAccproCloseYear'
close_year.security.max_retry_attempts: 3
close_year.security.transaction_timeout: 300
services:
_defaults:
@ -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

@ -1,261 +0,0 @@
# سیستم هوشمند هوش مصنوعی حسابیکس - نسخه 2.0
## مقدمه
سیستم جدید هوشمند هوش مصنوعی حسابیکس با رویکردی کاملاً متفاوت طراحی شده است. در این نسخه، به جای تشخیص دستی دستورات، به هوش مصنوعی گفته می‌شود که چه ابزارهایی دارد و اجازه داده می‌شود خودش تصمیم بگیرد که از کدام ابزار استفاده کند.
## ویژگی‌های کلیدی
### 🔧 تشخیص خودکار ابزارها
- هوش مصنوعی خودش تشخیص می‌دهد که چه ابزاری مناسب است
- نیازی به تشخیص دستی دستورات نیست
- انعطاف‌پذیری بالا در درک درخواست‌های کاربر
### 📝 پرامپ‌های سیستمی هوشمند
- پرامپ‌های جامع که تمام ابزارها را معرفی می‌کنند
- مثال‌های کاربردی برای هر ابزار
- قوانین و محدودیت‌های استفاده
### 🎯 تعامل چندمرحله‌ای
- امکان انجام عملیات پیچیده در چند مرحله
- جمع‌آوری اطلاعات تدریجی
- تجربه کاربری بهتر
## معماری سیستم
### 1. AIService (سرویس اصلی)
```php
class AIService {
// پرامپ سیستمی هوشمند
private function getSystemPrompt(): string
// پردازش پاسخ هوش مصنوعی
private function processAIResponse(string $aiResponse, ?Business $business, $user): array
// استخراج دستورات ابزار
private function extractToolCommands(string $aiResponse): array
// اجرای دستورات ابزار
private function executeToolCommand(array $command, ?Business $business, $user): array
}
```
### 2. PersonManagementService (مدیریت اشخاص)
```php
class PersonManagementService {
// ابزارهای مدیریت اشخاص
public function addPerson(array $params, Business $business, $user): array
public function deletePerson(array $params, Business $business, $user): array
public function editPerson(array $params, Business $business, $user): array
public function showPerson(array $params, Business $business, $user): array
public function searchPersons(array $params, Business $business): array
}
```
## ابزارهای موجود
### مدیریت اشخاص (person_management)
#### 1. افزودن شخص جدید
```bash
add_person{name:نام شخص}
```
**مثال‌ها:**
- `add_person{name:علی}`
- `add_person{name:احمد محمدی}`
#### 2. حذف شخص
```bash
delete_person{name:نام شخص}
```
**مثال‌ها:**
- `delete_person{name:علی}`
- `delete_person{name:محسن محمودی}`
#### 3. ویرایش شخص
```bash
edit_person{name:نام شخص, phone:موبایل, address:آدرس, email:ایمیل}
```
**مثال‌ها:**
- `edit_person{name:علی, phone:09123456789}`
- `edit_person{name:احمد, address:تهران، خیابان ولیعصر}`
#### 4. نمایش مشخصات
```bash
show_person{name:نام شخص}
```
**مثال‌ها:**
- `show_person{name:علی}`
- `show_person{name:محسن محمودی}`
#### 5. جستجوی اشخاص
```bash
search_persons{search:متن جستجو, limit:تعداد نتایج}
```
**مثال‌ها:**
- `search_persons{search:علی}`
- `search_persons{search:محمد, limit:5}`
## پرامپ سیستمی
پرامپ سیستمی شامل موارد زیر است:
### معرفی ابزارها
```
شما یک دستیار هوشمند برای سیستم حسابداری حسابیکس هستید. شما دسترسی به ابزارهای زیر دارید:
🔧 ابزارهای موجود:
1. **مدیریت اشخاص** (person_management):
- افزودن شخص جدید: add_person{name:نام شخص}
- حذف شخص: delete_person{name:نام شخص}
- ویرایش شخص: edit_person{name:نام شخص, phone:موبایل, address:آدرس, email:ایمیل}
- نمایش مشخصات: show_person{name:نام شخص}
- جستجوی اشخاص: search_persons{search:متن جستجو, limit:تعداد نتایج}
```
### قوانین استفاده
```
📋 قوانین استفاده:
- اگر کاربر درخواست عملیات مدیریت اشخاص دارد، از دستورات بالا استفاده کنید
- نام شخص می‌تواند نام مستعار یا نام کامل باشد
- برای عملیات پیچیده، ابتدا اطلاعات را جمع‌آوری کنید
- همیشه پاسخ فارسی و واضح ارائه دهید
```
### مثال‌های کاربردی
```
💡 مثال‌های استفاده:
- 'علی رو حذف کن' → delete_person{name:علی}
- 'شخص جدید با نام احمد اضافه کن' → add_person{name:احمد}
- 'مشخصات محسن رو نشون بده' → show_person{name:محسن}
```
## جریان کار
### 1. دریافت درخواست کاربر
```
کاربر: "شخص علی را حذف کن"
```
### 2. ساخت پرامپ هوشمند
```
پرامپ = پرامپ سیستمی + اطلاعات کسب و کار + سوال کاربر
```
### 3. ارسال به هوش مصنوعی
```
هوش مصنوعی پرامپ را دریافت کرده و تصمیم می‌گیرد که از ابزار مناسب استفاده کند
```
### 4. تشخیص دستورات ابزار
```
پاسخ هوش مصنوعی: "برای حذف شخص علی، از دستور delete_person{name:علی} استفاده می‌کنم."
```
### 5. استخراج و اجرای دستورات
```
دستور استخراج شده: delete_person{name:علی}
نتیجه اجرا: "شخص علی با موفقیت حذف شد."
```
### 6. ساخت پاسخ نهایی
```
پاسخ نهایی = پاسخ هوش مصنوعی + نتایج ابزارها
```
## مزایای سیستم جدید
### 🚀 هوشمندی بیشتر
- هوش مصنوعی خودش تصمیم می‌گیرد
- نیازی به تشخیص دستی دستورات نیست
- انعطاف‌پذیری بالا در درک درخواست‌ها
### 🔧 قابلیت توسعه
- افزودن ابزارهای جدید آسان است
- پرامپ‌ها قابل به‌روزرسانی هستند
- معماری مقیاس‌پذیر
### 🎯 تجربه کاربری بهتر
- تعامل طبیعی‌تر
- پاسخ‌های هوشمندانه‌تر
- پشتیبانی از عملیات پیچیده
### 🛡️ امنیت و کنترل
- تمام عملیات در لاگ ثبت می‌شود
- بررسی دسترسی کاربران
- کنترل خطاها
## توسعه آینده
### ابزارهای پیشنهادی
1. **مدیریت محصولات**
- افزودن، ویرایش، حذف محصولات
- مدیریت موجودی
- قیمت‌گذاری
2. **مدیریت تراکنش‌ها**
- ثبت تراکنش‌های مالی
- گزارش‌گیری
- تحلیل داده‌ها
3. **گزارش‌گیری هوشمند**
- گزارش‌های مالی
- تحلیل‌های آماری
- پیش‌بینی‌ها
4. **مدیریت حساب‌ها**
- مدیریت حساب‌های بانکی
- صندوق‌ها
- حقوق‌ها
### بهبودهای پیشنهادی
1. **یادگیری ماشین**
- بهبود تشخیص دستورات
- شخصی‌سازی پاسخ‌ها
- پیش‌بینی نیازهای کاربر
2. **پشتیبانی چندزبانه**
- پشتیبانی از زبان‌های مختلف
- تشخیص خودکار زبان
- ترجمه خودکار
3. **یکپارچه‌سازی پیشرفته**
- اتصال به سرویس‌های خارجی
- API های پیشرفته
- وب‌هوک‌ها
## نکات فنی
### مدیریت خطاها
- بررسی وجود کلیدهای مورد نیاز
- مدیریت خطاهای شبکه
- لاگ‌گیری کامل
### بهینه‌سازی عملکرد
- کش‌گذاری پاسخ‌ها
- کاهش درخواست‌های تکراری
- بهینه‌سازی پرامپ‌ها
### امنیت
- بررسی دسترسی کاربران
- اعتبارسنجی ورودی‌ها
- محافظت از داده‌های حساس
## نتیجه‌گیری
سیستم جدید هوشمند هوش مصنوعی حسابیکس با رویکردی نوآورانه و انعطاف‌پذیر طراحی شده است. این سیستم قابلیت توسعه بالایی دارد و می‌تواند به راحتی با نیازهای آینده سازگار شود.
مزایای اصلی این سیستم عبارتند از:
- هوشمندی بیشتر در تشخیص دستورات
- انعطاف‌پذیری بالا
- قابلیت توسعه آسان
- تجربه کاربری بهتر
- امنیت و کنترل بیشتر
این سیستم پایه‌ای محکم برای توسعه‌های آینده فراهم می‌کند و می‌تواند به عنوان یک دستیار هوشمند واقعی برای کاربران حسابیکس عمل کند.

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,41 @@
<?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 Version20250826214359 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 ADD CONSTRAINT FK_CC6A26EC40C1FEA7 FOREIGN KEY (year_id) REFERENCES year (id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_CC6A26EC40C1FEA7 ON import_workflow (year_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 DROP FOREIGN KEY FK_CC6A26EC40C1FEA7
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_CC6A26EC40C1FEA7 ON import_workflow
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
{
$process = new Process(['git', 'rev-parse', 'HEAD'], $this->rootDir);
$process->run();
if (!$process->isSuccessful()) {
$this->logger->warning('Failed to get current Git HEAD: ' . $process->getErrorOutput());
try {
$process = new Process(['git', 'rev-parse', 'HEAD'], $this->rootDir);
$process->run();
// If the command failed with "dubious ownership" error, try to fix it
if (!$process->isSuccessful() && str_contains($process->getErrorOutput(), 'dubious ownership')) {
if ($this->fixGitOwnershipIssue($this->rootDir)) {
// Retry the command after fixing ownership
$process = new Process(['git', 'rev-parse', 'HEAD'], $this->rootDir);
$process->run();
}
}
if (!$process->isSuccessful()) {
$this->logger->warning('Failed to get current Git HEAD: ' . $process->getErrorOutput());
return 'unknown';
}
return trim($process->getOutput());
} catch (\Exception $e) {
$this->logger->warning('Failed to get current Git HEAD: ' . $e->getMessage());
return 'unknown';
}
return trim($process->getOutput());
}
private function isUpToDate(): bool
@ -333,6 +433,16 @@ class UpdateSoftwareCommand extends Command
$this->runProcess(['git', 'fetch', 'origin'], $this->rootDir, new \Symfony\Component\Console\Output\NullOutput());
$process = new Process(['git', 'status', '-uno'], $this->rootDir);
$process->run();
// If the command failed with "dubious ownership" error, try to fix it
if (!$process->isSuccessful() && str_contains($process->getErrorOutput(), 'dubious ownership')) {
if ($this->fixGitOwnershipIssue($this->rootDir)) {
// Retry the command after fixing ownership
$process = new Process(['git', 'status', '-uno'], $this->rootDir);
$process->run();
}
}
$status = $process->getOutput();
return strpos($status, 'Your branch is up to date') !== false;
} catch (\Exception $e) {
@ -548,7 +658,19 @@ class UpdateSoftwareCommand extends Command
$this->runProcess(['git', '--version'], $this->rootDir, $output, 1);
$this->runProcess(['composer', '--version'], $this->rootDir, $output, 1, true);
$this->runProcess(['php', '-v'], $this->rootDir, $output, 1);
$this->runProcess(['npm', '--version'], $this->rootDir, $output, 1);
// Check npm with proper PATH
try {
$env = ['PATH' => '/usr/local/bin:/usr/bin:/bin:' . getenv('PATH')];
$process = new Process(['npm', '--version'], $this->rootDir, $env);
$process->setTimeout(30);
$process->mustRun();
$this->logger->info('Command executed successfully: npm --version');
$this->writeOutput($output, $process->getOutput());
} catch (ProcessFailedException $e) {
$this->logger->warning("Attempt 1 failed for npm --version: " . $e->getProcess()->getErrorOutput());
$this->writeOutput($output, "<comment>Warning: npm not found or not accessible. Frontend build may fail.</comment>");
}
$process = new Process(['whoami'], $this->rootDir);
$process->run();
@ -558,6 +680,16 @@ class UpdateSoftwareCommand extends Command
$this->logger->warning('Command executed as root user.');
}
// Check and fix Git ownership issues proactively
if (is_dir($this->rootDir . '/.git')) {
$this->writeOutput($output, 'Checking Git repository ownership...');
if ($this->fixGitOwnershipIssue($this->rootDir)) {
$this->writeOutput($output, '<info>Git ownership issue detected and fixed.</info>');
} else {
$this->writeOutput($output, '<info>Git repository ownership is correct.</info>');
}
}
$this->writeOutput($output, 'Pre-update checks completed successfully.');
}

View file

@ -19,6 +19,7 @@ use App\Entity\WalletTransaction;
use App\Service\Extractor;
use App\Service\Jdate;
use App\Service\JsonResp;
use App\Service\Log;
use App\Service\Notification;
use App\Service\Provider;
use App\Service\registryMGR;
@ -359,6 +360,9 @@ class AdminController extends AbstractController
'passChequeInput' => $registryMGR->get('sms', 'plugAccproPassChequeInput'),
'rejectChequeInput' => $registryMGR->get('sms', 'plugAccproRejectChequeInput')
];
$resp['plugWarranty'] = [
'sendSerial' => $registryMGR->get('sms', 'plugWarrantySendSerial'),
];
return $this->json($resp);
}
@ -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,916 @@
<?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]);
// محاسبه تجمیعی موجودی کالا بر اساس ردیف‌های حسابداری تاییدشده
$qb = $entityManager->createQueryBuilder();
$qb->select('c.id AS cid, SUM(CASE WHEN d.type IN (:plusTypes) THEN COALESCE(r.commdityCount, 0) WHEN d.type IN (:minusTypes) THEN -COALESCE(r.commdityCount, 0) ELSE 0 END) AS stock')
->from(\App\Entity\HesabdariRow::class, 'r')
->join('r.doc', 'd')
->join('r.commodity', 'c')
->where('r.bid = :bid')
->andWhere('d.isApproved = :approved')
->groupBy('c.id')
->setParameter('bid', $business)
->setParameter('approved', true)
->setParameter('plusTypes', ['buy', 'open_balance', 'rfsell'])
->setParameter('minusTypes', ['sell', 'rfbuy']);
$stockResults = $qb->getQuery()->getArrayResult();
$stockMap = [];
foreach ($stockResults as $sr) {
$stockMap[(int)$sr['cid']] = (float)$sr['stock'];
}
$row = 2;
foreach ($commodities as $commodity) {
// محاسبه موجودی و ارزش اسمی
$count = 0.0;
if (!$commodity->isKhadamat()) {
$count = $stockMap[$commodity->getId()] ?? 0.0;
}
$priceSell = $commodity->getPriceSell();
$priceSell = is_numeric($priceSell) ? (float)$priceSell : 0.0;
$nominalValue = $count * $priceSell;
$data = [
$commodity->getName(),
$commodity->getCode(),
$commodity->getCat() ? $commodity->getCat()->getName() : '',
$commodity->getUnit() ? $commodity->getUnit()->getName() : '',
$commodity->getPriceBuy(),
$commodity->getPriceSell(),
$count,
$nominalValue,
$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();
$bs += (float) $item->getBs();
$bd += (float) $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,7 +425,10 @@ class BankController extends AbstractController
'year' => $acc['year']
]);
if ($prs) {
$transactions[] = $prs;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $prs->getDoc()->isApproved()) {
$transactions[] = $prs;
}
}
}
}
@ -327,19 +440,24 @@ class BankController extends AbstractController
'تاریخ',
'توضیحات',
'شرح سند',
'تفضیل',
'تفصیل',
'طرف حساب‌ها',
'بستانکار',
'بدهکار',
'سال مالی',
]
];
foreach ($transactions as $transaction) {
// استخراج طرف حساب‌ها برای این تراکنش
$counterpartAccounts = $this->getCounterpartAccountsForTransaction($transaction, $bank, $entityManager);
$arrayEntity[] = [
$transaction->getId(),
$transaction->getDoc()->getDate(),
$transaction->getDes(),
$transaction->getDoc()->getDes(),
$transaction->getRef()->getName(),
$counterpartAccounts,
$transaction->getBs(),
$transaction->getBd(),
$transaction->getYear()->getlabel()
@ -353,6 +471,57 @@ class BankController extends AbstractController
return new BinaryFileResponse($filePath);
}
/**
* استخراج طرف حساب‌های مربوط به یک تراکنش
*/
private function getCounterpartAccountsForTransaction($transaction, $bank, EntityManagerInterface $entityManager): string
{
$doc = $transaction->getDoc();
$bankCode = $bank->getCode();
// دریافت تمام ردیف‌های مربوط به این سند
$docRows = $entityManager->getRepository(HesabdariRow::class)
->createQueryBuilder('hr')
->leftJoin('hr.bank', 'ba')
->leftJoin('hr.cashdesk', 'cd')
->leftJoin('hr.salary', 's')
->leftJoin('hr.person', 'p')
->where('hr.doc = :doc')
->setParameter('doc', $doc)
->getQuery()
->getResult();
$accounts = [];
foreach ($docRows as $docRow) {
// بررسی اینکه آیا این ردیف طرف حساب است (نه بانک انتخابی)
$isCounterpart = false;
$accountName = '';
if ($docRow->getBank()) {
if ($docRow->getBank()->getCode() != $bankCode) {
$isCounterpart = true;
$accountName = 'بانک: ' . $docRow->getBank()->getName();
}
} elseif ($docRow->getCashdesk()) {
$isCounterpart = true;
$accountName = 'صندوق: ' . $docRow->getCashdesk()->getName();
} elseif ($docRow->getSalary()) {
$isCounterpart = true;
$accountName = 'تنخواه: ' . $docRow->getSalary()->getName();
} elseif ($docRow->getPerson()) {
$isCounterpart = true;
$accountName = 'شخص: ' . $docRow->getPerson()->getNikename();
}
if ($isCounterpart) {
$amount = $docRow->getBd() > 0 ? $docRow->getBd() : $docRow->getBs();
$accounts[] = $accountName . ' (' . number_format($amount, 0, '.', ',') . ')';
}
}
return implode(' | ', $accounts);
}
#[Route('/api/bank/card/list/print', name: 'app_bank_card_list_print')]
public function app_bank_card_list_print(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
@ -369,12 +538,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 +570,19 @@ class BankController extends AbstractController
'year'=>$acc['year']
]);
if ($prs) {
$transactions[] = $prs;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $prs->getDoc()->isApproved()) {
$transactions[] = $prs;
}
}
}
}
// اضافه کردن طرف حساب‌ها به هر تراکنش
foreach ($transactions as $transaction) {
$transaction->counterpartAccounts = $this->getCounterpartAccountsForTransaction($transaction, $bank, $entityManager);
}
$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,28 @@ class BusinessController extends AbstractController
}
}
// Approval settings
if (array_key_exists('requireTwoStepApproval', $params))
$business->setRequireTwoStepApproval((bool)$params['requireTwoStepApproval'] ?? false);
else
$business->setRequireTwoStepApproval(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 +297,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 +584,13 @@ class BusinessController extends AbstractController
'plugHrmDocs' => true,
'plugGhestaManager' => true,
'plugTaxSettings' => true,
'plugWarranty' => true,
'plugImportWorkflow' => true,
'inquiry' => true,
'ai' => true,
'importWorkflow' => true,
'plugHrmAttendance' => true,
'storehelper' => true,
];
} elseif ($perm) {
$result = [
@ -591,8 +635,13 @@ 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(),
'importWorkflow' => $perm->isImportWorkflow(),
'plugHrmAttendance' => $perm->isPlugHrmAttendance(),
'storehelper' => $perm->isStorehelper()
];
}
return $this->json($result);
@ -662,9 +711,14 @@ 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->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 +758,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 +773,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 +796,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 +819,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 +842,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;
$temp['relatedDocsCount'] = count($item->getRelatedDocs());
$pays = 0;
foreach ($item->getRelatedDocs() as $relatedDoc) {
$pays += $relatedDoc->getAmount();
}
$temp['relatedDocsPays'] = $pays;
// محاسبه پرداختی‌ها
$relatedDocs = $entityManager->getRepository(HesabdariDoc::class)
->createQueryBuilder('rd')
->select('SUM(rd.amount) as total_pays, COUNT(rd.id) as count_docs')
->innerJoin('rd.relatedDocs', 'rel')
->where('rel.id = :sourceDocId')
->andWhere('rd.bid = :bidId')
->setParameter('sourceDocId', $doc['id'])
->setParameter('bidId', $acc['bid']->getId())
->getQuery()
->getSingleResult();
$temp['commodities'] = [];
foreach ($item->getHesabdariRows() as $item) {
if ($item->getRef()->getCode() == '51') {
$temp['discountAll'] = $item->getBs();
} elseif ($item->getRef()->getCode() == '90') {
$temp['transferCost'] = $item->getBd();
}
if ($item->getCommodity()) {
$temp['commodities'][] = Explore::ExploreCommodity($item->getCommodity(), $item->getCommdityCount());
$item['relatedDocsCount'] = (int) $relatedDocs['count_docs'];
$item['relatedDocsPays'] = $relatedDocs['total_pays'] ?? 0;
// محاسبه کالاها و تخفیف/هزینه حمل
$item['commodities'] = [];
$item['discountAll'] = 0;
$item['transferCost'] = 0;
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy(['doc' => $doc['id']]);
foreach ($rows as $row) {
if ($row->getRef()->getCode() == '51') {
$item['discountAll'] = $row->getBs();
} elseif ($row->getRef()->getCode() == '90') {
$item['transferCost'] = $row->getBd();
} elseif ($row->getCommodity()) {
$item['commodities'][] = Explore::ExploreCommodity($row->getCommodity(), $row->getCommdityCount());
}
}
if (!array_key_exists('discountAll', $temp))
$temp['discountAll'] = 0;
if (!array_key_exists('transferCost', $temp))
$temp['transferCost'] = 0;
$dataTemp[] = $temp;
$dataTemp[] = $item;
}
return $this->json($dataTemp);
return $this->json([
'items' => $dataTemp,
'total' => (int) $totalItems,
'page' => $page,
'perPage' => $perPage,
]);
}
#[Route('/api/buy/posprinter/invoice', name: 'app_buy_posprinter_invoice')]
@ -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,12 +49,20 @@ 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();
$bs += (float) $item->getBs();
$bd += (float) $item->getBd();
}
$data->setBalance($bd - $bs);
$resp[] = Explore::ExploreCashdesk($data);
@ -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,12 +194,20 @@ 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();
$bs += (float) $item->getBs();
$bd += (float) $item->getBd();
}
$data->setBalance($bd - $bs);
}
@ -211,13 +236,27 @@ 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();
$bd += $item->getBd();
$bs += (float) $item->getBs();
$bd += (float) $item->getBd();
}
return $this->json([
@ -251,11 +290,20 @@ class CashdeskController extends AbstractController
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.cashdesk = :cashdesk')
->andWhere('r.bid = :bid')
->setParameter('cashdesk', $cashdesk)
->setParameter('bid', $acc['bid']);
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
if (isset($params['startDate']) && isset($params['endDate'])) {
$query->andWhere('r.doc.date BETWEEN :startDate AND :endDate')
->setParameter('startDate', $params['startDate'])
@ -293,12 +341,29 @@ class CashdeskController extends AbstractController
$cashdesk = $entityManager->getRepository(Cashdesk::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$cashdesk)
throw $this->createNotFoundException();
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'cashdesk' => $cashdesk,
'year'=>$acc['year']
]);
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.cashdesk = :cashdesk')
->andWhere('r.year = :year')
->setParameter('bid', $acc['bid'])
->setParameter('cashdesk', $cashdesk)
->setParameter('year', $acc['year']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$transactions = $query->getQuery()->getResult();
} else {
$transactions = [];
foreach ($params['items'] as $param) {
@ -309,7 +374,10 @@ class CashdeskController extends AbstractController
'year' => $acc['year']
]);
if ($prs) {
$transactions[] = $prs;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $prs->getDoc()->isApproved()) {
$transactions[] = $prs;
}
}
}
}
@ -321,7 +389,7 @@ class CashdeskController extends AbstractController
'تاریخ',
'توضیحات',
'شرح سند',
'تفضیل',
'تفصیل',
'بستانکار',
'بدهکار',
'سال مالی',
@ -370,12 +438,28 @@ class CashdeskController extends AbstractController
if (!$cashdesk)
throw $this->createNotFoundException();
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'cashdesk' => $cashdesk,
'year'=>$acc['year']
]);
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->join('r.doc', 'd')
->where('r.bid = :bid')
->andWhere('r.cashdesk = :cashdesk')
->andWhere('r.year = :year')
->setParameter('bid', $acc['bid'])
->setParameter('cashdesk', $cashdesk)
->setParameter('year', $acc['year']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$transactions = $query->getQuery()->getResult();
} else {
$transactions = [];
foreach ($params['items'] as $param) {
@ -386,7 +470,10 @@ class CashdeskController extends AbstractController
'year'=>$acc['year']
]);
if ($prs) {
$transactions[] = $prs;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $prs->getDoc()->isApproved()) {
$transactions[] = $prs;
}
}
}
}

View file

@ -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' => 1,
'result' => 1,
'createdItems' => $createdItems
]);
} else {
return $this->json([
'Success' => 1,
'result' => 1
]);
}
return $this->json([
'Success' => true,
'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'));
}
} else {
$data = $entityManager->getRepository(Commodity::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
if (!$data)
throw $this->createNotFoundException();
$commodityService = new \App\Cog\CommodityService($entityManager);
$result = $commodityService->addOrUpdateCommodity($params, $acc, $id);
if (isset($result['error'])) {
return $this->json($result, 400);
}
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,7 +428,10 @@ class CostController extends AbstractController
'money' => $acc['money']
]);
if ($doc) {
$items[] = $doc;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $doc->isApproved()) {
$items[] = $doc;
}
}
}
}
@ -429,14 +463,30 @@ class CostController extends AbstractController
$params = json_decode($request->getContent(), true) ?? [];
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
// دریافت آیتم‌های انتخاب شده یا همه آیتم‌ها
if (!isset($params['items'])) {
$items = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'type' => 'cost',
'year' => $acc['year'],
'money' => $acc['money']
]);
$query = $entityManager->createQueryBuilder()
->select('d')
->from(HesabdariDoc::class, 'd')
->where('d.bid = :bid')
->andWhere('d.type = :type')
->andWhere('d.year = :year')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('type', 'cost')
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$items = $query->getQuery()->getResult();
} else {
$items = [];
foreach ($params['items'] as $param) {
@ -448,7 +498,10 @@ class CostController extends AbstractController
'money' => $acc['money']
]);
if ($doc) {
$items[] = $doc;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $doc->isApproved()) {
$items[] = $doc;
}
}
}
}
@ -566,6 +619,20 @@ class CostController extends AbstractController
$doc->setMoney($acc['money']);
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
// Set approval status based on business settings
$business = $acc['bid'];
if ($business->isRequireTwoStepApproval()) {
// Two-step approval is enabled
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
} else {
// Two-step approval is disabled - auto approve
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
}
$entityManager->persist($doc);
$entityManager->flush();

View file

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

View file

@ -42,6 +42,20 @@ class DirectHesabdariDoc extends AbstractController
$hesabdariDoc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
$hesabdariDoc->setDateSubmit(time());
// Set approval status based on business settings
$business = $acc['bid'];
if ($business->isRequireTwoStepApproval()) {
// Two-step approval is enabled
$hesabdariDoc->setIsPreview(true);
$hesabdariDoc->setIsApproved(false);
$hesabdariDoc->setApprovedBy(null);
} else {
// Two-step approval is disabled - auto approve
$hesabdariDoc->setIsPreview(false);
$hesabdariDoc->setIsApproved(true);
$hesabdariDoc->setApprovedBy($this->getUser());
}
//insert rows
if (isset($prams['rows'])) {
if (count($prams['rows']) < 2) {
@ -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'
];
$note = '';
$printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid' => $bid]);
if ($printSettings) {
$note = $printSettings->getSellNoteString();
$pdfPid = 0;
// فیلد جدید وضعیت حساب مشتری
$personItems = $entityManager->getRepository(HesabdariRow::class)->findBy(['bid' => $bid, 'person' => $person]);
$accountStatus = [];
$bs = 0;
$bd = 0;
foreach ($personItems as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
}
$pdfPid = $provider->createPrint(
$bid,
$bid->getOwner(),
$this->renderView('pdf/printers/sell.html.twig', [
'bid' => $bid,
'doc' => $doc,
'rows' => $doc->getHesabdariRows(),
'person' => $person,
if ($bs > $bd) {
$accountStatus['label'] = 'بستانکار';
$accountStatus['value'] = $bs - $bd;
} else {
$accountStatus['label'] = 'بدهکار';
$accountStatus['value'] = $bd - $bs;
}
$business = $entityManager->getRepository(Business::class)->find($bid);
$twoApproval = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
if ($twoApproval && $doc->isApproved() !== true && $doc->isPreview() == true) {
return $this->render('bundles/TwigBundle/Exception/error.html.twig', [
'message' => 'فاکتور هنوز تایید نشده است'
]);
}
// پیدا کردن مالک کسب و کار
$businessOwner = $bus->getOwner();
if ($params['pdf'] == true || $params['printers'] == true) {
$note = '';
if ($printSettings) {
$note = $printSettings->getSellNoteString();
}
// Build safe context data for rendering
$rowsArr = array_map(function ($row) {
return [
'commodity' => $row->getCommodity() ? [
'name' => method_exists($row->getCommodity(), 'getName') ? $row->getCommodity()->getName() : null,
'code' => method_exists($row->getCommodity(), 'getCode') ? $row->getCommodity()->getCode() : null,
] : null,
'commodityCount' => $row->getCommdityCount(),
'des' => $row->getDes(),
'bs' => $row->getBs(),
'tax' => $row->getTax(),
'discount' => $row->getDiscount(),
'showPercentDiscount' => $row->getDiscountType() === 'percent',
'discountPercent' => $row->getDiscountPercent()
];
}, $doc->getHesabdariRows()->toArray());
$personArr = $person ? [
'name' => $person->getName(),
'mobile' => $person->getMobile(),
'tel' => $person->getTel(),
'address' => $person->getAddress(),
] : null;
$businessArr = $bus ? [
'name' => method_exists($bus, 'getName') ? $bus->getName() : null,
'nikename' => method_exists($bus, 'getNikename') ? $bus->getNikename() : null,
'tel' => method_exists($bus, 'getTel') ? $bus->getTel() : null,
'mobile' => method_exists($bus, 'getMobile') ? $bus->getMobile() : null,
'address' => method_exists($bus, 'getAddress') ? $bus->getAddress() : null,
'shenasemeli' => method_exists($bus, 'getShenasemeli') ? $bus->getShenasemeli() : null,
'codeeghtesadi' => method_exists($bus, 'getCodeeghtesadi') ? $bus->getCodeeghtesadi() : null,
'id' => method_exists($bus, 'getId') ? $bus->getId() : null,
] : null;
$context = [
'accountStatus' => $accountStatus,
'business' => $businessArr,
'bid' => $businessArr,
'doc' => [
'code' => $doc->getCode(),
'date' => method_exists($doc, 'getDate') ? $doc->getDate() : null,
'taxPercent' => method_exists($doc, 'getTaxPercent') ? $doc->getTaxPercent() : null,
'discountPercent' => $doc->getDiscountPercent(),
'discountType' => $doc->getDiscountType(),
'amount' => $doc->getAmount(),
'money' => [
'shortName' => method_exists($doc, 'getMoney') && $doc->getMoney() && method_exists($doc->getMoney(), 'getShortName') ? $doc->getMoney()->getShortName() : null,
],
],
'rows' => $rowsArr,
'person' => $personArr,
'discount' => $discount,
'transfer' => $transfer,
'printOptions' => $printOptions,
'note' => $note
]),
false,
$printOptions['paper']
);
'note' => $note,
];
// Decide template: custom or default
$html = null;
// Check if custom invoice plugin is available and active
$isCustomInvoiceActive = false;
$selectedTemplate = null;
// Use injected services if available
if ($pluginService) {
$isCustomInvoiceActive = $pluginService->isActive('custominvoice', $bid);
$selectedTemplate = $printSettings ? $printSettings->getSellTemplate() : null;
}
if ($isCustomInvoiceActive && $selectedTemplate && class_exists('App\Entity\CustomInvoiceTemplate') && $selectedTemplate instanceof \App\Entity\CustomInvoiceTemplate) {
if ($renderer) {
$html = $renderer->render($selectedTemplate->getCode() ?? '', $context);
}
}
if ($html === null) {
// fallback to default Twig template
$html = $this->renderView('pdf/printers/sell.html.twig', [
'accountStatus' => $accountStatus,
'bid' => $bus,
'doc' => $doc,
'rows' => array_map(function ($row) {
return [
'commodity' => $row->getCommodity(),
'commodityCount' => $row->getCommdityCount(),
'des' => $row->getDes(),
'bs' => $row->getBs(),
'tax' => $row->getTax(),
'discount' => $row->getDiscount(),
'showPercentDiscount' => $row->getDiscountType() === 'percent',
'discountPercent' => $row->getDiscountPercent()
];
}, $doc->getHesabdariRows()->toArray()),
'person' => $person,
'printInvoice' => $params['printers'],
'discount' => $discount,
'transfer' => $transfer,
'printOptions' => $printOptions,
'note' => $note,
'showPercentDiscount' => $doc->getDiscountType() === 'percent',
'discountPercent' => $doc->getDiscountPercent()
]);
}
$pdfPid = $provider->createPrint(
$bus,
$businessOwner, // مالک کسب و کار
$html,
false,
$printOptions['paper']
);
}
if ($params['posPrint'] == true) {
$pid = $provider->createPrint(
$bus,
$businessOwner, // مالک کسب و کار
$this->renderView('pdf/posPrinters/justSell.html.twig', [
'bid' => $bus,
'doc' => $doc,
'rows' => array_map(function ($row) {
return [
'commodity' => $row->getCommodity(),
'commodityCount' => $row->getCommdityCount(),
'des' => $row->getDes(),
'bs' => $row->getBs(),
'tax' => $row->getTax(),
'discount' => $row->getDiscount(),
'showPercentDiscount' => $row->getDiscountType() === 'percent',
'discountPercent' => $row->getDiscountPercent()
];
}, $doc->getHesabdariRows()->toArray()),
'discount' => $discount,
'showPercentDiscount' => $doc->getDiscountType() === 'percent',
'discountPercent' => $doc->getDiscountPercent()
]),
false
);
}
return $this->redirectToRoute('app_front_print', ['id' => $pdfPid]);
}
}

View file

@ -52,12 +52,26 @@ class HesabdariController extends AbstractController
$acc = $access->hasRole('accounting');
if (!$acc)
throw $this->createAccessDeniedException();
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['code'],
'money' => $acc['money']
]);
// Check if we should include preview documents
$includePreview = $params['includePreview'] ?? false;
if ($includePreview) {
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['code'],
'money' => $acc['money']
]);
} else {
// Default: only approved documents - استفاده از متد ایمن
$doc = $provider->findHesabdariDocSafely($entityManager, [
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['code'],
'money' => $acc['money']
]);
}
if (!$doc)
throw $this->createNotFoundException();
//add shortlink to doc
@ -188,7 +202,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 +222,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 +235,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 +353,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
@ -408,200 +440,254 @@ class HesabdariController extends AbstractController
throw $this->createNotFoundException('rows is to short');
if (!array_key_exists('date', $params) || !array_key_exists('des', $params))
throw $this->createNotFoundException('some params mistake');
if (array_key_exists('update', $params) && $params['update'] != '') {
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['update'],
'money' => $acc['money']
]);
if (!$doc)
throw $this->createNotFoundException('document not found.');
$doc->setDes($params['des']);
$doc->setDate($params['date']);
$doc->setMoney($acc['money']);
if (array_key_exists('refData', $params))
$doc->setRefData($params['refData']);
if (array_key_exists('plugin', $params))
$doc->setPlugin($params['plugin']);
$entityManager->persist($doc);
$entityManager->flush();
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy([
'doc' => $doc
]);
foreach ($rows as $row)
$entityManager->remove($row);
} else {
$doc = new HesabdariDoc();
$doc->setBid($acc['bid']);
$doc->setYear($acc['year']);
$doc->setDes($params['des']);
$doc->setDateSubmit(time());
$doc->setType($params['type']);
$doc->setDate($params['date']);
$doc->setSubmitter($this->getUser());
$doc->setMoney($acc['money']);
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
if (array_key_exists('refData', $params))
$doc->setRefData($params['refData']);
if (array_key_exists('plugin', $params))
$doc->setPlugin($params['plugin']);
$entityManager->persist($doc);
$entityManager->flush();
}
//add document to related docs
if (array_key_exists('related', $params)) {
$relatedDoc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'code' => $params['related'],
'bid' => $doc->getBid(),
'money' => $acc['money']
]);
if ($relatedDoc) {
$relatedDoc->addRelatedDoc($doc);
$entityManager->persist($relatedDoc);
$entityManager->flush();
}
}
$amount = 0;
foreach ($params['rows'] as $row) {
$row['bs'] = str_replace(',', '', $row['bs']);
$row['bd'] = str_replace(',', '', $row['bd']);
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs($row['bs']);
$hesabdariRow->setBd($row['bd']);
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
'code' => $row['table']
]);
$hesabdariRow->setRef($ref);
$entityManager->persist($hesabdariRow);
if (array_key_exists('referral', $row))
$hesabdariRow->setReferral($row['referral']);
$amount += $row['bs'];
//check is type is person
if ($row['type'] == 'person') {
$person = $entityManager->getRepository(Person::class)->find($row['id']);
if (!$person)
throw $this->createNotFoundException('person not found');
elseif ($person->getBid()->getId() != $acc['bid']->getId())
throw $this->createAccessDeniedException('person is not in this business');
$hesabdariRow->setPerson($person);
} elseif ($row['type'] == 'cheque') {
$person = $entityManager->getRepository(Person::class)->findOneBy([
// شروع تراکنش
$entityManager->beginTransaction();
try {
if (array_key_exists('update', $params) && $params['update'] != '') {
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'id' => $row['chequeOwner']
'year' => $acc['year'],
'code' => $params['update'],
'money' => $acc['money']
]);
$cheque = new Cheque();
$cheque->setBid($acc['bid']);
$cheque->setSubmitter($this->getUser());
$cheque->setPayDate($row['chequeDate']);
$cheque->setBankOncheque($row['chequeBank']);
$cheque->setRef($hesabdariRow->getRef());
$cheque->setNumber($row['chequeNum']);
$cheque->setSayadNum($row['chequeSayadNum']);
$cheque->setDateSubmit(time());
$cheque->setDes($row['des']);
$dateArray = explode('/', $row['chequeDate']);
$dateGre = strtotime($jdate->jalali_to_gregorian($dateArray['0'], $dateArray['1'], $dateArray['2'], '/'));
$cheque->setDateStamp($dateGre);
$cheque->setPerson($person);
$cheque->setRef($entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => $row['table']]));
$cheque->setType($row['chequeType']);
if ($cheque->getType() == 'input')
$cheque->setAmount($hesabdariRow->getBd());
else
$cheque->setAmount($hesabdariRow->getBs());
$cheque->setLocked(false);
$cheque->setRejected(false);
$cheque->setStatus('پاس نشده');
$entityManager->persist($cheque);
if (!$doc)
throw $this->createNotFoundException('document not found.');
$doc->setDes($params['des']);
$doc->setDate($params['date']);
$doc->setMoney($acc['money']);
if (array_key_exists('refData', $params))
$doc->setRefData($params['refData']);
if (array_key_exists('plugin', $params))
$doc->setPlugin($params['plugin']);
$entityManager->persist($doc);
$entityManager->flush();
$hesabdariRow->setCheque($cheque);
} elseif ($row['type'] == 'bank') {
$bank = $entityManager->getRepository(BankAccount::class)->findOneBy([
'id' => $row['id'],
'bid' => $acc['bid']
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy([
'doc' => $doc
]);
if (!$bank)
throw $this->createNotFoundException('bank not found');
$hesabdariRow->setBank($bank);
} elseif ($row['type'] == 'salary') {
$salary = $entityManager->getRepository(Salary::class)->find($row['id']);
if (!$salary)
throw $this->createNotFoundException('salary not found');
elseif ($salary->getBid()->getId() != $acc['bid']->getId())
throw $this->createAccessDeniedException('bank is not in this business');
$hesabdariRow->setSalary($salary);
} elseif ($row['type'] == 'cashdesk') {
$cashdesk = $entityManager->getRepository(Cashdesk::class)->find($row['id']);
if (!$cashdesk)
throw $this->createNotFoundException('cashdesk not found');
elseif ($cashdesk->getBid()->getId() != $acc['bid']->getId())
throw $this->createAccessDeniedException('bank is not in this business');
$hesabdariRow->setCashdesk($cashdesk);
} elseif ($row['type'] == 'commodity') {
$row['count'] = str_replace(',', '', $row['count']);
$commodity = $entityManager->getRepository(Commodity::class)->find($row['commodity']['id']);
if (!$commodity)
throw $this->createNotFoundException('commodity not found');
elseif ($commodity->getBid()->getId() != $acc['bid']->getId())
throw $this->createAccessDeniedException('$commodity is not in this business');
$hesabdariRow->setCommodity($commodity);
$hesabdariRow->setCommdityCount($row['count']);
foreach ($rows as $row)
$entityManager->remove($row);
} else {
$doc = new HesabdariDoc();
$doc->setBid($acc['bid']);
$doc->setYear($acc['year']);
$doc->setDes($params['des']);
$doc->setDateSubmit(time());
$doc->setType($params['type']);
$doc->setDate($params['date']);
$doc->setSubmitter($this->getUser());
$doc->setMoney($acc['money']);
// تولید کد منحصر به فرد با مدیریت خطا
try {
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
} catch (\Exception $e) {
$entityManager->rollback();
return $this->json([
'result' => 0,
'msg' => 'خطا در تولید کد سند: ' . $e->getMessage()
]);
}
// وضعیت تایید: اگر autoApprove=true ارسال شده باشد، اجباری تایید شود
$autoApprove = isset($params['autoApprove']) ? (bool)$params['autoApprove'] : null;
$business = $acc['bid'];
if ($autoApprove === true) {
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
} elseif ($autoApprove === false) {
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
} else {
// پیش‌فرض مطابق تنظیمات کسب‌وکار
if ($business->isRequireTwoStepApproval()) {
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
} else {
$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))
$doc->setPlugin($params['plugin']);
$entityManager->persist($doc);
$entityManager->flush();
}
if (array_key_exists('plugin', $row))
$hesabdariRow->setPlugin($row['plugin']);
if (array_key_exists('refData', $row))
$hesabdariRow->setRefData($row['refData']);
$hesabdariRow->setDes($row['des']);
$entityManager->persist($hesabdariRow);
$entityManager->flush();
}
$doc->setAmount($amount);
$entityManager->persist($doc);
//check ghesta
if (array_key_exists('ghestaId', $params)) {
$ghesta = $entityManager->getRepository(PlugGhestaDoc::class)->find($params['ghestaId']);
if ($ghesta) {
$ghestaItem = $entityManager->getRepository(PlugGhestaItem::class)->findOneBy([
'doc' => $ghesta,
'num' => $params['ghestaNum']
//add document to related docs
if (array_key_exists('related', $params)) {
$relatedDoc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'code' => $params['related'],
'bid' => $doc->getBid(),
'money' => $acc['money']
]);
if ($ghestaItem) {
$ghestaItem->setHesabdariDoc($doc);
$entityManager->persist($ghestaItem);
if ($relatedDoc) {
$relatedDoc->addRelatedDoc($doc);
$entityManager->persist($relatedDoc);
$entityManager->flush();
}
}
}
$entityManager->flush();
$log->insert(
'حسابداری',
'سند حسابداری شماره ' . $doc->getCode() . ' ثبت / ویرایش شد.',
$this->getUser(),
$request->headers->get('activeBid'),
$doc
);
return $this->json([
'result' => 1,
'doc' => $provider->Entity2Array($doc, 0)
]);
$amount = 0;
foreach ($params['rows'] as $row) {
$row['bs'] = str_replace(',', '', $row['bs']);
$row['bd'] = str_replace(',', '', $row['bd']);
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs($row['bs']);
$hesabdariRow->setBd($row['bd']);
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
'code' => $row['table']
]);
$hesabdariRow->setRef($ref);
$entityManager->persist($hesabdariRow);
if (array_key_exists('referral', $row))
$hesabdariRow->setReferral($row['referral']);
$amount += $row['bs'];
//check is type is person
if ($row['type'] == 'person') {
$person = $entityManager->getRepository(Person::class)->find($row['id']);
if (!$person)
throw $this->createNotFoundException('person not found');
elseif ($person->getBid()->getId() != $acc['bid']->getId())
throw $this->createAccessDeniedException('person is not in this business');
$hesabdariRow->setPerson($person);
} elseif ($row['type'] == 'cheque') {
$person = $entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'id' => $row['chequeOwner']
]);
$cheque = new Cheque();
$cheque->setBid($acc['bid']);
$cheque->setSubmitter($this->getUser());
$cheque->setPayDate($row['chequeDate']);
$cheque->setBankOncheque($row['chequeBank']);
$cheque->setRef($hesabdariRow->getRef());
$cheque->setNumber($row['chequeNum']);
$cheque->setSayadNum($row['chequeSayadNum']);
$cheque->setDateSubmit(time());
$cheque->setDes($row['des']);
$dateArray = explode('/', $row['chequeDate']);
$dateGre = strtotime($jdate->jalali_to_gregorian($dateArray['0'], $dateArray['1'], $dateArray['2'], '/'));
$cheque->setDateStamp($dateGre);
$cheque->setPerson($person);
$cheque->setRef($entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => $row['table']]));
$cheque->setType($row['chequeType']);
if ($cheque->getType() == 'input')
$cheque->setAmount($hesabdariRow->getBd());
else
$cheque->setAmount($hesabdariRow->getBs());
$cheque->setLocked(false);
$cheque->setRejected(false);
$cheque->setStatus('پاس نشده');
$entityManager->persist($cheque);
$entityManager->flush();
$hesabdariRow->setCheque($cheque);
} elseif ($row['type'] == 'bank') {
$bank = $entityManager->getRepository(BankAccount::class)->findOneBy([
'id' => $row['id'],
'bid' => $acc['bid']
]);
if (!$bank)
throw $this->createNotFoundException('bank not found');
$hesabdariRow->setBank($bank);
} elseif ($row['type'] == 'salary') {
$salary = $entityManager->getRepository(Salary::class)->find($row['id']);
if (!$salary)
throw $this->createNotFoundException('salary not found');
elseif ($salary->getBid()->getId() != $acc['bid']->getId())
throw $this->createAccessDeniedException('bank is not in this business');
$hesabdariRow->setSalary($salary);
} elseif ($row['type'] == 'cashdesk') {
$cashdesk = $entityManager->getRepository(Cashdesk::class)->find($row['id']);
if (!$cashdesk)
throw $this->createNotFoundException('cashdesk not found');
elseif ($cashdesk->getBid()->getId() != $acc['bid']->getId())
throw $this->createAccessDeniedException('bank is not in this business');
$hesabdariRow->setCashdesk($cashdesk);
} elseif ($row['type'] == 'commodity') {
$row['count'] = str_replace(',', '', $row['count']);
$commodity = $entityManager->getRepository(Commodity::class)->find($row['commodity']['id']);
if (!$commodity)
throw $this->createNotFoundException('commodity not found');
elseif ($commodity->getBid()->getId() != $acc['bid']->getId())
throw $this->createAccessDeniedException('$commodity is not in this business');
$hesabdariRow->setCommodity($commodity);
$hesabdariRow->setCommdityCount($row['count']);
}
if (array_key_exists('plugin', $row))
$hesabdariRow->setPlugin($row['plugin']);
if (array_key_exists('refData', $row))
$hesabdariRow->setRefData($row['refData']);
$hesabdariRow->setDes($row['des']);
$entityManager->persist($hesabdariRow);
$entityManager->flush();
}
$doc->setAmount($amount);
$entityManager->persist($doc);
//check ghesta
if (array_key_exists('ghestaId', $params)) {
$ghesta = $entityManager->getRepository(PlugGhestaDoc::class)->find($params['ghestaId']);
if ($ghesta) {
$ghestaItem = $entityManager->getRepository(PlugGhestaItem::class)->findOneBy([
'doc' => $ghesta,
'num' => $params['ghestaNum']
]);
if ($ghestaItem) {
$ghestaItem->setHesabdariDoc($doc);
$entityManager->persist($ghestaItem);
}
}
}
// ثبت تراکنش
$entityManager->flush();
$entityManager->commit();
$log->insert(
'حسابداری',
'سند حسابداری شماره ' . $doc->getCode() . ' ثبت / ویرایش شد.',
$this->getUser(),
$request->headers->get('activeBid'),
$doc
);
return $this->json([
'result' => 1,
'doc' => $provider->Entity2Array($doc, 0)
]);
} catch (\Exception $e) {
// در صورت بروز خطا، تراکنش را rollback کن
$entityManager->rollback();
return $this->json([
'result' => 0,
'msg' => 'خطا در ثبت سند: ' . $e->getMessage()
]);
}
}
#[Route('/api/accounting/remove', name: 'app_accounting_remove_doc')]
public function app_accounting_remove_doc(Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
public function app_accounting_remove_doc(Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, Provider $provider): JsonResponse
{
$params = [];
if ($content = $request->getContent()) {
@ -609,10 +695,15 @@ class HesabdariController extends AbstractController
}
if (!array_key_exists('code', $params))
$this->createNotFoundException();
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'code' => $params['code'],
'bid' => $request->headers->get('activeBid')
]);
// استفاده از query builder برای پیدا کردن سند
$doc = $entityManager->getRepository(HesabdariDoc::class)
->createQueryBuilder('h')
->where('h.code = :code')
->andWhere('h.bid = :bid')
->setParameter('code', $params['code'])
->setParameter('bid', $request->headers->get('activeBid'))
->getQuery()
->getOneOrNullResult();
if (!$doc)
throw $this->createNotFoundException();
$roll = '';
@ -848,99 +939,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 +973,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 +1126,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 +1158,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 +1211,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,
]
@ -1385,4 +1403,69 @@ class HesabdariController extends AbstractController
return $this->json(['Success' => true, 'data' => $tree]);
}
/**
* بررسی وضعیت کدهای تکراری
*/
#[Route('/api/accounting/check-duplicate-codes', name: 'app_accounting_check_duplicate_codes', methods: ['GET'])]
public function checkDuplicateCodes(Provider $provider, Request $request, Access $access): JsonResponse
{
$acc = $access->hasRole('admin'); // فقط ادمین می‌تواند این کار را انجام دهد
if (!$acc) {
throw $this->createAccessDeniedException();
}
$part = $request->query->get('part', 'accounting');
try {
$hasDuplicates = $provider->hasDuplicateCodes($acc['bid'], $part);
return $this->json([
'result' => 1,
'has_duplicates' => $hasDuplicates,
'message' => $hasDuplicates ? 'کدهای تکراری یافت شد' : 'کدهای تکراری یافت نشد'
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => 'خطا در بررسی کدهای تکراری: ' . $e->getMessage()
]);
}
}
/**
* ترمیم کدهای تکراری حسابداری
*/
#[Route('/api/accounting/fix-duplicate-codes', name: 'app_accounting_fix_duplicate_codes', methods: ['POST'])]
public function fixDuplicateCodes(Provider $provider, Request $request, Access $access): JsonResponse
{
$acc = $access->hasRole('admin'); // فقط ادمین می‌تواند این کار را انجام دهد
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$part = $params['part'] ?? 'accounting';
try {
$result = $provider->fixDuplicateCodes($acc['bid'], $part);
return $this->json([
'result' => $result['success'] ? 1 : 0,
'message' => $result['message'],
'fixed_count' => $result['fixed_count'] ?? 0
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => 'خطا در ترمیم کدهای تکراری: ' . $e->getMessage()
]);
}
}
}

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,7 +425,10 @@ class IncomeController extends AbstractController
'money' => $acc['money']
]);
if ($doc) {
$items[] = $doc;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $doc->isApproved()) {
$items[] = $doc;
}
}
}
}
@ -426,14 +460,30 @@ class IncomeController extends AbstractController
$params = json_decode($request->getContent(), true) ?? [];
// Check if includePreview parameter is provided
$includePreview = $params['includePreview'] ?? false;
// دریافت آیتم‌های انتخاب شده یا همه آیتم‌ها
if (!isset($params['items'])) {
$items = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'type' => 'income',
'year' => $acc['year'],
'money' => $acc['money']
]);
$query = $entityManager->createQueryBuilder()
->select('d')
->from(HesabdariDoc::class, 'd')
->where('d.bid = :bid')
->andWhere('d.type = :type')
->andWhere('d.year = :year')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('type', 'income')
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
if (!$includePreview) {
// Default: only show approved documents
$query->andWhere('d.isApproved = :isApproved')
->setParameter('isApproved', true);
}
$items = $query->getQuery()->getResult();
} else {
$items = [];
foreach ($params['items'] as $param) {
@ -445,7 +495,10 @@ class IncomeController extends AbstractController
'money' => $acc['money']
]);
if ($doc) {
$items[] = $doc;
// Check if the document is approved (unless includePreview is true)
if ($includePreview || $doc->isApproved()) {
$items[] = $doc;
}
}
}
}
@ -563,6 +616,20 @@ class IncomeController extends AbstractController
$doc->setMoney($acc['money']);
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
// Set approval status based on business settings
$business = $acc['bid'];
if ($business->isRequireTwoStepApproval()) {
// Two-step approval is enabled
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
} else {
// Two-step approval is disabled - auto approve
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
}
$entityManager->persist($doc);
$entityManager->flush();

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,9 @@ namespace App\Controller;
use App\Entity\Plugin;
use App\Entity\PluginProdect;
use App\Entity\Business;
use App\Service\Access;
use App\Service\PluginService;
use App\Service\Extractor;
use App\Service\Jdate;
use App\Service\Log;
@ -23,6 +25,23 @@ class PluginController extends AbstractController
{
private const PRICE_MULTIPLIER = 10; // ضریب قیمت به صورت ثابت برای محاسبه تبدیل تومان به ریال
#[Route('/api/plugin/check/{plugin}/{bid}', name: 'api_plugin_check')]
public function api_plugin_check($plugin, $bid,Access $access, PluginService $pluginService, EntityManagerInterface $entityManager): Response
{
$acc = $access->hasRole('join');
if (!$acc) {
return $this->json(['active' => false]);
}
$business = $entityManager->getRepository(Business::class)->find($bid);
if (!$business) {
return $this->json(['active' => false]);
}
$isActive = $pluginService->isActive($plugin, $business);
return $this->json(['active' => $isActive]);
}
/**
* بررسی دسترسی کاربر با نقش مشخص
*
@ -266,7 +285,6 @@ class PluginController extends AbstractController
foreach ($plugins as $plugin) {
$plugin->setDateExpire($jdate->jdate('Y/n/d', $plugin->getDateExpire()));
$plugin->setDateSubmit($jdate->jdate('Y/n/d', $plugin->getDateSubmit()));
$plugin->setPrice(number_format($plugin->getPrice()));
}
return $this->json($plugins);
@ -429,4 +447,101 @@ class PluginController extends AbstractController
return $this->json($result);
}
#[Route('/api/admin/plugins/sync', name: 'api_admin_plugins_sync', methods: ["POST"])]
public function api_admin_plugins_sync(EntityManagerInterface $entityManager): JsonResponse
{
$pluginData = [
[
'name' => 'بسته حسابداری پیشرفته',
'code' => 'accpro',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '200000',
'icon' => 'accpro.png',
'defaultOn' => null,
],
[
'name' => 'افزونه مدیریت تعمیرگاه(تعمیرکاران)',
'code' => 'repservice',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '200000',
'icon' => 'repservice.jpg',
'defaultOn' => null,
],
[
'name' => 'افزونه طراحی فاکتور اختصاصی',
'code' => 'custominvoice',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '200000',
'icon' => 'custominvoice.png',
'defaultOn' => null,
],
[
'name' => 'افزونه فروش اقساطی',
'code' => 'ghesta',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '95000',
'icon' => 'ghesta.png',
'defaultOn' => null,
],
[
'name' => 'سامانه مودیان',
'code' => 'taxsettings',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '200000',
'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);
foreach ($pluginData as $data) {
$exists = $repo->findOneBy(['code' => $data['code']]);
if (!$exists) {
$plugin = new PluginProdect();
$plugin->setName($data['name'])
->setCode($data['code'])
->setTimestamp($data['timestamp'])
->setTimelabel($data['timelabel'])
->setPrice($data['price'])
->setIcon($data['icon'])
->setDefaultOn($data['defaultOn']);
$entityManager->persist($plugin);
}
}
$entityManager->flush();
return $this->json(['status' => 'done']);
}
}

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

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