Compare commits
2 commits
6d3832f682
...
0fb64e8cfa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fb64e8cfa | ||
|
|
b2766b6d46 |
119
docs/DUPLICATE_CODE_FIX.md
Normal file
119
docs/DUPLICATE_CODE_FIX.md
Normal 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**: عملکرد سیستم را پس از اعمال تغییرات نظارت کنید
|
||||||
146
docs/accounting/DUPLICATE_CODE_ERROR_FIX.md
Normal file
146
docs/accounting/DUPLICATE_CODE_ERROR_FIX.md
Normal 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` را به طور کامل حل میکند و سیستم را در برابر کدهای تکراری محافظت میکند.
|
||||||
107
docs/accounting/NUMERIC_CODE_GUIDELINES.md
Normal file
107
docs/accounting/NUMERIC_CODE_GUIDELINES.md
Normal 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 کدهای تکراری موجود را پیدا کرده و با کدهای عددی منحصر به فرد جایگزین میکند.
|
||||||
129
docs/accounting/REASONABLE_CODE_GENERATION.md
Normal file
129
docs/accounting/REASONABLE_CODE_GENERATION.md
Normal 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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
این تغییرات کدهای حسابداری را مطابق با عرف و استانداردهای حسابداری میکند.
|
||||||
155
docs/accounting/SAFE_ENTITY_FINDING.md
Normal file
155
docs/accounting/SAFE_ENTITY_FINDING.md
Normal 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` را در تمام متدها حل میکند و سیستم را در برابر کدهای تکراری محافظت میکند.
|
||||||
|
|
@ -52,6 +52,7 @@ class HesabdariController extends AbstractController
|
||||||
$acc = $access->hasRole('accounting');
|
$acc = $access->hasRole('accounting');
|
||||||
if (!$acc)
|
if (!$acc)
|
||||||
throw $this->createAccessDeniedException();
|
throw $this->createAccessDeniedException();
|
||||||
|
|
||||||
// Check if we should include preview documents
|
// Check if we should include preview documents
|
||||||
$includePreview = $params['includePreview'] ?? false;
|
$includePreview = $params['includePreview'] ?? false;
|
||||||
|
|
||||||
|
|
@ -63,8 +64,8 @@ class HesabdariController extends AbstractController
|
||||||
'money' => $acc['money']
|
'money' => $acc['money']
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
// Default: only approved documents
|
// Default: only approved documents - استفاده از متد ایمن
|
||||||
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
$doc = $provider->findHesabdariDocSafely($entityManager, [
|
||||||
'bid' => $acc['bid'],
|
'bid' => $acc['bid'],
|
||||||
'year' => $acc['year'],
|
'year' => $acc['year'],
|
||||||
'code' => $params['code'],
|
'code' => $params['code'],
|
||||||
|
|
@ -439,6 +440,11 @@ class HesabdariController extends AbstractController
|
||||||
throw $this->createNotFoundException('rows is to short');
|
throw $this->createNotFoundException('rows is to short');
|
||||||
if (!array_key_exists('date', $params) || !array_key_exists('des', $params))
|
if (!array_key_exists('date', $params) || !array_key_exists('des', $params))
|
||||||
throw $this->createNotFoundException('some params mistake');
|
throw $this->createNotFoundException('some params mistake');
|
||||||
|
|
||||||
|
// شروع تراکنش
|
||||||
|
$entityManager->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
if (array_key_exists('update', $params) && $params['update'] != '') {
|
if (array_key_exists('update', $params) && $params['update'] != '') {
|
||||||
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
||||||
'bid' => $acc['bid'],
|
'bid' => $acc['bid'],
|
||||||
|
|
@ -473,7 +479,17 @@ class HesabdariController extends AbstractController
|
||||||
$doc->setDate($params['date']);
|
$doc->setDate($params['date']);
|
||||||
$doc->setSubmitter($this->getUser());
|
$doc->setSubmitter($this->getUser());
|
||||||
$doc->setMoney($acc['money']);
|
$doc->setMoney($acc['money']);
|
||||||
|
|
||||||
|
// تولید کد منحصر به فرد با مدیریت خطا
|
||||||
|
try {
|
||||||
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
|
$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
|
// Set approval status based on business settings
|
||||||
$business = $acc['bid'];
|
$business = $acc['bid'];
|
||||||
|
|
@ -631,7 +647,11 @@ class HesabdariController extends AbstractController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ثبت تراکنش
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
|
$entityManager->commit();
|
||||||
|
|
||||||
$log->insert(
|
$log->insert(
|
||||||
'حسابداری',
|
'حسابداری',
|
||||||
'سند حسابداری شماره ' . $doc->getCode() . ' ثبت / ویرایش شد.',
|
'سند حسابداری شماره ' . $doc->getCode() . ' ثبت / ویرایش شد.',
|
||||||
|
|
@ -644,10 +664,20 @@ class HesabdariController extends AbstractController
|
||||||
'result' => 1,
|
'result' => 1,
|
||||||
'doc' => $provider->Entity2Array($doc, 0)
|
'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')]
|
#[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 = [];
|
$params = [];
|
||||||
if ($content = $request->getContent()) {
|
if ($content = $request->getContent()) {
|
||||||
|
|
@ -655,7 +685,8 @@ class HesabdariController extends AbstractController
|
||||||
}
|
}
|
||||||
if (!array_key_exists('code', $params))
|
if (!array_key_exists('code', $params))
|
||||||
$this->createNotFoundException();
|
$this->createNotFoundException();
|
||||||
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
// استفاده از متد ایمن برای پیدا کردن سند
|
||||||
|
$doc = $provider->findHesabdariDocSafely($entityManager, [
|
||||||
'code' => $params['code'],
|
'code' => $params['code'],
|
||||||
'bid' => $request->headers->get('activeBid')
|
'bid' => $request->headers->get('activeBid')
|
||||||
]);
|
]);
|
||||||
|
|
@ -1358,4 +1389,69 @@ class HesabdariController extends AbstractController
|
||||||
|
|
||||||
return $this->json(['Success' => true, 'data' => $tree]);
|
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()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,19 +81,415 @@ class Provider
|
||||||
|
|
||||||
public function getAccountingCode($bid, $part)
|
public function getAccountingCode($bid, $part)
|
||||||
{
|
{
|
||||||
|
$maxRetries = 10; // حداکثر تعداد تلاش برای تولید کد منحصر به فرد
|
||||||
|
$retryCount = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$retryCount++;
|
||||||
|
|
||||||
|
// شروع تراکنش
|
||||||
|
$this->entityManager->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
$setter = 'set' . ucfirst($part) . 'Code';
|
$setter = 'set' . ucfirst($part) . 'Code';
|
||||||
$part = 'get' . ucfirst($part) . 'Code';
|
$part = 'get' . ucfirst($part) . 'Code';
|
||||||
|
|
||||||
$business = $this->entityManager->getRepository(Business::class)->find($bid);
|
$business = $this->entityManager->getRepository(Business::class)->find($bid);
|
||||||
if (!$business)
|
if (!$business) {
|
||||||
|
$this->entityManager->rollback();
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$count = $business->{$part}();
|
$count = $business->{$part}();
|
||||||
if (is_null($count))
|
if (is_null($count))
|
||||||
$count = 1000;
|
$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->persist($business);
|
||||||
$this->entityManager->flush();
|
$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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
79
hesabixCore/tests/ProviderTest.php
Normal file
79
hesabixCore/tests/ProviderTest.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue