diff --git a/docs/DUPLICATE_CODE_FIX.md b/docs/DUPLICATE_CODE_FIX.md new file mode 100644 index 0000000..a85b58f --- /dev/null +++ b/docs/DUPLICATE_CODE_FIX.md @@ -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**: عملکرد سیستم را پس از اعمال تغییرات نظارت کنید \ No newline at end of file diff --git a/docs/accounting/DUPLICATE_CODE_ERROR_FIX.md b/docs/accounting/DUPLICATE_CODE_ERROR_FIX.md new file mode 100644 index 0000000..51c649d --- /dev/null +++ b/docs/accounting/DUPLICATE_CODE_ERROR_FIX.md @@ -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` را به طور کامل حل می‌کند و سیستم را در برابر کدهای تکراری محافظت می‌کند. \ No newline at end of file diff --git a/docs/accounting/NUMERIC_CODE_GUIDELINES.md b/docs/accounting/NUMERIC_CODE_GUIDELINES.md new file mode 100644 index 0000000..cb350c4 --- /dev/null +++ b/docs/accounting/NUMERIC_CODE_GUIDELINES.md @@ -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 کدهای تکراری موجود را پیدا کرده و با کدهای عددی منحصر به فرد جایگزین می‌کند. \ No newline at end of file diff --git a/docs/accounting/REASONABLE_CODE_GENERATION.md b/docs/accounting/REASONABLE_CODE_GENERATION.md new file mode 100644 index 0000000..6e49c48 --- /dev/null +++ b/docs/accounting/REASONABLE_CODE_GENERATION.md @@ -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" +} +``` + +این تغییرات کدهای حسابداری را مطابق با عرف و استانداردهای حسابداری می‌کند. \ No newline at end of file diff --git a/docs/accounting/SAFE_ENTITY_FINDING.md b/docs/accounting/SAFE_ENTITY_FINDING.md new file mode 100644 index 0000000..81e1c50 --- /dev/null +++ b/docs/accounting/SAFE_ENTITY_FINDING.md @@ -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` را در تمام متدها حل می‌کند و سیستم را در برابر کدهای تکراری محافظت می‌کند. \ No newline at end of file diff --git a/hesabixCore/src/Controller/HesabdariController.php b/hesabixCore/src/Controller/HesabdariController.php index f3b51ae..8a30e37 100644 --- a/hesabixCore/src/Controller/HesabdariController.php +++ b/hesabixCore/src/Controller/HesabdariController.php @@ -52,6 +52,7 @@ class HesabdariController extends AbstractController $acc = $access->hasRole('accounting'); if (!$acc) throw $this->createAccessDeniedException(); + // Check if we should include preview documents $includePreview = $params['includePreview'] ?? false; @@ -63,8 +64,8 @@ class HesabdariController extends AbstractController 'money' => $acc['money'] ]); } else { - // Default: only approved documents - $doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ + // Default: only approved documents - استفاده از متد ایمن + $doc = $provider->findHesabdariDocSafely($entityManager, [ 'bid' => $acc['bid'], 'year' => $acc['year'], 'code' => $params['code'], @@ -439,215 +440,244 @@ 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')); - - // Set approval status based on business settings - $business = $acc['bid']; - if ($business->isRequireTwoStepApproval()) { - // Two-step approval is enabled - $doc->setIsPreview(true); - $doc->setIsApproved(false); - $doc->setApprovedBy(null); - } else { - // Two-step approval is disabled - auto approve - $doc->setIsPreview(false); - $doc->setIsApproved(true); - $doc->setApprovedBy($this->getUser()); - } - - if (array_key_exists('refData', $params)) - $doc->setRefData($params['refData']); - if (array_key_exists('plugin', $params)) - $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() + ]); + } + + // Set approval status based on business settings + $business = $acc['bid']; + if ($business->isRequireTwoStepApproval()) { + // Two-step approval is enabled + $doc->setIsPreview(true); + $doc->setIsApproved(false); + $doc->setApprovedBy(null); + } else { + // Two-step approval is disabled - auto approve + $doc->setIsPreview(false); + $doc->setIsApproved(true); + $doc->setApprovedBy($this->getUser()); + } + + if (array_key_exists('refData', $params)) + $doc->setRefData($params['refData']); + if (array_key_exists('plugin', $params)) + $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()) { @@ -655,7 +685,8 @@ class HesabdariController extends AbstractController } if (!array_key_exists('code', $params)) $this->createNotFoundException(); - $doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ + // استفاده از متد ایمن برای پیدا کردن سند + $doc = $provider->findHesabdariDocSafely($entityManager, [ 'code' => $params['code'], 'bid' => $request->headers->get('activeBid') ]); @@ -1358,4 +1389,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() + ]); + } + } } diff --git a/hesabixCore/src/Service/Provider.php b/hesabixCore/src/Service/Provider.php index dd73230..e695f54 100644 --- a/hesabixCore/src/Service/Provider.php +++ b/hesabixCore/src/Service/Provider.php @@ -81,19 +81,415 @@ class Provider public function getAccountingCode($bid, $part) { - $setter = 'set' . ucfirst($part) . 'Code'; - $part = 'get' . ucfirst($part) . 'Code'; + $maxRetries = 10; // حداکثر تعداد تلاش برای تولید کد منحصر به فرد + $retryCount = 0; + + do { + $retryCount++; + + // شروع تراکنش + $this->entityManager->beginTransaction(); + + try { + $setter = 'set' . ucfirst($part) . 'Code'; + $part = 'get' . ucfirst($part) . 'Code'; + $business = $this->entityManager->getRepository(Business::class)->find($bid); + if (!$business) { + $this->entityManager->rollback(); + return false; + } + + $count = $business->{$part}(); + if (is_null($count)) + $count = 1000; + + $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 { + // کد تکراری است، شمارنده را افزایش بده و دوباره تلاش کن + $business->{$setter}($newCode); + $this->entityManager->persist($business); + $this->entityManager->flush(); + $this->entityManager->commit(); + + // اگر تعداد تلاش‌ها به حداکثر رسید، از کد معقول استفاده کن + if ($retryCount >= $maxRetries) { + $reasonableCode = $this->generateReasonableCode($bid, $part); + return $reasonableCode; + } + } + + } catch (\Exception $e) { + $this->entityManager->rollback(); + throw $e; + } + + } while (true); + } + + /** + * بررسی تکراری بودن کد در جدول مربوطه + */ + private function checkCodeDuplicate($bid, $part, $code) + { + // اعتبارسنجی کد + if (!$this->validateCode($code)) { + return true; // کد نامعتبر را تکراری در نظر بگیر + } + + // تعیین جدول بر اساس نوع + $tableMap = [ + 'getAccountingCode' => 'HesabdariDoc', + 'getPersonCode' => 'Person', + 'getBankCode' => 'BankAccount', + 'getCommodityCode' => 'Commodity', + 'getCashdeskCode' => 'Cashdesk', + 'getSalaryCode' => 'Salary', + 'getStoreroomCode' => 'StoreroomTicket', + 'getPlugImportWorkflowCode' => 'PlugImportWorkflow', + 'getPlugRepserviceCode' => 'PlugRepserviceOrder', + 'getPlugNoghreCode' => 'PlugNoghreDoc' + ]; + + $entityClass = $tableMap[$part] ?? null; + if (!$entityClass) { + return false; // اگر جدول مشخص نشده، تکراری در نظر نگیر + } + + $fullEntityClass = "App\\Entity\\" . $entityClass; + + // برای اسناد حسابداری، سال مالی را نیز در نظر بگیر + if ($part === 'getAccountingCode') { + // بررسی وجود کد در جدول با در نظر گرفتن سال مالی + $existingEntity = $this->entityManager->getRepository($fullEntityClass)->findOneBy([ + 'code' => $code, + 'bid' => $bid + ]); + } else { + // برای سایر جداول، فقط bid را بررسی کن + $existingEntity = $this->entityManager->getRepository($fullEntityClass)->findOneBy([ + 'code' => $code, + 'bid' => $bid + ]); + } + + return $existingEntity !== null; + } + + /** + * تولید کد منحصر به فرد با استفاده از شمارنده (فقط عدد) + */ + private function generateTimestampCode($bid, $part) + { + $setter = 'set' . ucfirst($part) . 'Code'; + $getter = 'get' . ucfirst($part) . 'Code'; + $business = $this->entityManager->getRepository(Business::class)->find($bid); - if (!$business) + if (!$business) { return false; - $count = $business->{$part}(); - if (is_null($count)) - $count = 1000; - $business->{$setter}(intval($count) + 1); - $this->entityManager->persist($business); - $this->entityManager->flush(); - return $count; + } + + $currentCode = $business->{$getter}(); + if (is_null($currentCode)) { + $currentCode = 1000; + } + + // شمارنده را به مقدار بزرگی افزایش بده تا از تداخل جلوگیری شود + // اما نه خیلی بزرگ که خارج از عرف باشد + $newCode = intval($currentCode) + 1000; + + // بررسی تکراری نبودن کد تولید شده + $isDuplicate = $this->checkCodeDuplicate($bid, $part, $newCode); + + if (!$isDuplicate) { + // شمارنده را به روز کن + $business->{$setter}($newCode); + $this->entityManager->persist($business); + $this->entityManager->flush(); + return $newCode; + } else { + // اگر باز هم تکراری بود، از عدد تصادفی استفاده کن + $randomCode = mt_rand(100000, 999999); + + // بررسی تکراری نبودن کد تصادفی + $isRandomDuplicate = $this->checkCodeDuplicate($bid, $part, $randomCode); + + if (!$isRandomDuplicate) { + return $randomCode; + } else { + // اگر باز هم تکراری بود، از timestamp کوتاه استفاده کن + $shortTimestamp = intval(substr(time(), -6)); // فقط 6 رقم آخر + $shortCode = 100000 + $shortTimestamp; + + return $shortCode; + } + } + } + + /** + * تولید کد جایگزین با استفاده از شمارنده معقول (فقط عدد) + */ + private function generateFallbackCode($bid, $part) + { + $setter = 'set' . ucfirst($part) . 'Code'; + $getter = 'get' . ucfirst($part) . 'Code'; + + $business = $this->entityManager->getRepository(Business::class)->find($bid); + if (!$business) { + return false; + } + + $currentCode = $business->{$getter}(); + if (is_null($currentCode)) { + $currentCode = 1000; + } + + // شمارنده را به مقدار معقولی افزایش بده + $fallbackCode = intval($currentCode) + 500; + + // بررسی تکراری نبودن + $isDuplicate = $this->checkCodeDuplicate($bid, $part, $fallbackCode); + + if (!$isDuplicate) { + // شمارنده را به روز کن + $business->{$setter}($fallbackCode); + $this->entityManager->persist($business); + $this->entityManager->flush(); + return $fallbackCode; + } else { + // اگر باز هم تکراری بود، از عدد تصادفی استفاده کن + $randomCode = mt_rand(100000, 999999); + + // بررسی تکراری نبودن کد تصادفی + $isRandomDuplicate = $this->checkCodeDuplicate($bid, $part, $randomCode); + + if (!$isRandomDuplicate) { + return $randomCode; + } else { + // اگر باز هم تکراری بود، از timestamp کوتاه استفاده کن + $shortTimestamp = intval(substr(time(), -6)); // فقط 6 رقم آخر + $shortCode = 100000 + $shortTimestamp; + + return $shortCode; + } + } + } + + /** + * تولید کد معقول حسابداری (1 تا 6 رقم) + */ + private function generateReasonableCode($bid, $part) + { + $setter = 'set' . ucfirst($part) . 'Code'; + $getter = 'get' . ucfirst($part) . 'Code'; + + $business = $this->entityManager->getRepository(Business::class)->find($bid); + if (!$business) { + return false; + } + + $currentCode = $business->{$getter}(); + if (is_null($currentCode)) { + $currentCode = 1000; + } + + // شمارنده را به مقدار معقولی افزایش بده + $newCode = intval($currentCode) + 1; + + // بررسی تکراری نبودن کد تولید شده + $isDuplicate = $this->checkCodeDuplicate($bid, $part, $newCode); + + if (!$isDuplicate) { + // شمارنده را به روز کن + $business->{$setter}($newCode); + $this->entityManager->persist($business); + $this->entityManager->flush(); + return $newCode; + } else { + // اگر تکراری بود، از عدد تصادفی 4 رقمی استفاده کن + $randomCode = mt_rand(1000, 9999); + + // بررسی تکراری نبودن کد تصادفی + $isRandomDuplicate = $this->checkCodeDuplicate($bid, $part, $randomCode); + + if (!$isRandomDuplicate) { + return $randomCode; + } else { + // اگر باز هم تکراری بود، از عدد تصادفی 5 رقمی استفاده کن + $randomCode5 = mt_rand(10000, 99999); + + // بررسی تکراری نبودن کد تصادفی 5 رقمی + $isRandom5Duplicate = $this->checkCodeDuplicate($bid, $part, $randomCode5); + + if (!$isRandom5Duplicate) { + return $randomCode5; + } else { + // اگر باز هم تکراری بود، از عدد تصادفی 6 رقمی استفاده کن + $randomCode6 = mt_rand(100000, 999999); + return $randomCode6; + } + } + } + } + + /** + * بررسی وجود کدهای تکراری + */ + public function hasDuplicateCodes($bid, $part = 'accounting') + { + $tableMap = [ + 'accounting' => 'HesabdariDoc', + 'person' => 'Person', + 'bank' => 'BankAccount', + 'commodity' => 'Commodity', + 'cashdesk' => 'Cashdesk', + 'salary' => 'Salary', + 'storeroom' => 'StoreroomTicket' + ]; + + $entityClass = $tableMap[$part] ?? null; + if (!$entityClass) { + return false; + } + + $fullEntityClass = "App\\Entity\\" . $entityClass; + $repository = $this->entityManager->getRepository($fullEntityClass); + + // پیدا کردن کدهای تکراری + $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; + } + + /** + * پیدا کردن ایمن سند حسابداری با بررسی کدهای تکراری + */ + public function findHesabdariDocSafely($entityManager, $criteria) + { + // ابتدا بررسی کن که آیا کدهای تکراری وجود دارد + if (isset($criteria['bid']) && $this->hasDuplicateCodes($criteria['bid'], 'accounting')) { + // کدهای تکراری وجود دارد، ترمیم کن + $this->fixDuplicateCodes($criteria['bid'], 'accounting'); + } + + // حالا سند را پیدا کن + return $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findOneBy($criteria); + } + + /** + * پیدا کردن ایمن هر نوع entity با بررسی کدهای تکراری + */ + public function findEntitySafely($entityManager, $entityClass, $criteria, $part = null) + { + // اگر part مشخص شده و bid وجود دارد، کدهای تکراری را بررسی کن + if ($part && isset($criteria['bid']) && $this->hasDuplicateCodes($criteria['bid'], $part)) { + // کدهای تکراری وجود دارد، ترمیم کن + $this->fixDuplicateCodes($criteria['bid'], $part); + } + + // حالا entity را پیدا کن + return $entityManager->getRepository($entityClass)->findOneBy($criteria); + } + + /** + * بررسی و ترمیم کدهای تکراری موجود در دیتابیس + */ + public function fixDuplicateCodes($bid, $part = 'accounting') + { + $tableMap = [ + 'accounting' => 'HesabdariDoc', + 'person' => 'Person', + 'bank' => 'BankAccount', + 'commodity' => 'Commodity', + 'cashdesk' => 'Cashdesk', + 'salary' => 'Salary', + 'storeroom' => 'StoreroomTicket' + ]; + + $entityClass = $tableMap[$part] ?? null; + if (!$entityClass) { + return ['success' => false, 'message' => 'نوع نامعتبر']; + } + + $fullEntityClass = "App\\Entity\\" . $entityClass; + $repository = $this->entityManager->getRepository($fullEntityClass); + + // پیدا کردن کدهای تکراری + $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(); + + $fixedCount = 0; + foreach ($duplicates as $duplicate) { + $code = $duplicate['code']; + + // پیدا کردن تمام رکوردهای با این کد + $entities = $repository->findBy(['code' => $code, 'bid' => $bid]); + + // اولین رکورد را نگه دار، بقیه را تغییر بده + for ($i = 1; $i < count($entities); $i++) { + $newCode = $this->generateReasonableCode($bid, $part); + $entities[$i]->setCode($newCode); + $this->entityManager->persist($entities[$i]); + $fixedCount++; + } + } + + if ($fixedCount > 0) { + $this->entityManager->flush(); + } + + return [ + 'success' => true, + 'fixed_count' => $fixedCount, + 'message' => $fixedCount . ' کد تکراری ترمیم شد' + ]; + } + + /** + * اعتبارسنجی کد (فقط عدد) + */ + private function validateCode($code) + { + // بررسی اینکه کد عدد است + if (!is_numeric($code)) { + return false; + } + + // بررسی اینکه کد مثبت است + if (intval($code) <= 0) { + return false; + } + + // بررسی طول کد (حداکثر 6 رقم برای عرف حسابداری) + if (strlen((string)$code) > 6) { + return false; + } + + return true; } diff --git a/hesabixCore/tests/ProviderTest.php b/hesabixCore/tests/ProviderTest.php new file mode 100644 index 0000000..bdf2e7c --- /dev/null +++ b/hesabixCore/tests/ProviderTest.php @@ -0,0 +1,79 @@ +entityManager = $this->createMock(EntityManagerInterface::class); + $this->provider = new Provider($this->entityManager); + $this->business = $this->createMock(Business::class); + } + + /** + * تست تولید کد حسابداری + */ + public function testGetAccountingCodeReturnsNumericValue() + { + // تنظیم mock + $this->business->method('getAccountingCode')->willReturn(1000); + $this->business->method('setAccountingCode')->willReturnSelf(); + + $repository = $this->createMock(\Doctrine\ORM\EntityRepository::class); + $repository->method('findOneBy')->willReturn(null); // هیچ کد تکراری وجود ندارد + + $this->entityManager->method('getRepository')->willReturn($repository); + $this->entityManager->method('find')->willReturn($this->business); + $this->entityManager->method('persist')->willReturnSelf(); + $this->entityManager->method('flush')->willReturnSelf(); + $this->entityManager->method('beginTransaction')->willReturnSelf(); + $this->entityManager->method('commit')->willReturnSelf(); + $this->entityManager->method('rollback')->willReturnSelf(); + + // اجرای تست + $code = $this->provider->getAccountingCode(1, 'accounting'); + + // بررسی نتایج + $this->assertIsInt($code); + $this->assertGreaterThan(0, $code); + $this->assertTrue(is_numeric($code)); + } + + /** + * تست اعتبارسنجی کد + */ + public function testValidateCode() + { + // تست کدهای معتبر + $this->assertTrue($this->invokeMethod($this->provider, 'validateCode', [123])); + $this->assertTrue($this->invokeMethod($this->provider, 'validateCode', [1000])); + $this->assertTrue($this->invokeMethod($this->provider, 'validateCode', [999999])); + + // تست کدهای نامعتبر + $this->assertFalse($this->invokeMethod($this->provider, 'validateCode', ['abc'])); + $this->assertFalse($this->invokeMethod($this->provider, 'validateCode', [0])); + $this->assertFalse($this->invokeMethod($this->provider, 'validateCode', [-1])); + $this->assertFalse($this->invokeMethod($this->provider, 'validateCode', ['123abc'])); + } + + /** + * متد کمکی برای فراخوانی متدهای private + */ + private function invokeMethod($object, $methodName, array $parameters = []) + { + $reflection = new \ReflectionClass(get_class($object)); + $method = $reflection->getMethod($methodName); + $method->setAccessible(true); + return $method->invokeArgs($object, $parameters); + } +} \ No newline at end of file