bug fix in generate accounting codes

This commit is contained in:
Hesabix 2025-08-28 12:16:28 +00:00
parent f37aca7c6e
commit b2766b6d46
8 changed files with 1436 additions and 209 deletions

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,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

@ -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,6 +440,11 @@ 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');
// شروع تراکنش
$entityManager->beginTransaction();
try {
if (array_key_exists('update', $params) && $params['update'] != '') {
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
@ -473,7 +479,17 @@ class HesabdariController extends AbstractController
$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'];
@ -631,7 +647,11 @@ class HesabdariController extends AbstractController
}
}
}
// ثبت تراکنش
$entityManager->flush();
$entityManager->commit();
$log->insert(
'حسابداری',
'سند حسابداری شماره ' . $doc->getCode() . ' ثبت / ویرایش شد.',
@ -644,10 +664,20 @@ class HesabdariController extends AbstractController
'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()
]);
}
}
}

View file

@ -81,19 +81,415 @@ class Provider
public function getAccountingCode($bid, $part)
{
$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)
if (!$business) {
$this->entityManager->rollback();
return false;
}
$count = $business->{$part}();
if (is_null($count))
$count = 1000;
$business->{$setter}(intval($count) + 1);
$newCode = intval($count) + 1;
// بررسی تکراری بودن کد در جدول مربوطه
$isDuplicate = $this->checkCodeDuplicate($bid, $part, $newCode);
if (!$isDuplicate) {
// کد منحصر به فرد است، شمارنده را افزایش بده
$business->{$setter}($newCode);
$this->entityManager->persist($business);
$this->entityManager->flush();
return $count;
$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) {
return false;
}
$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;
}

View file

@ -0,0 +1,79 @@
<?php
namespace App\Tests;
use App\Service\Provider;
use App\Entity\Business;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
class ProviderTest extends TestCase
{
private $entityManager;
private $provider;
private $business;
protected function setUp(): void
{
$this->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);
}
}