Compare commits
No commits in common. "0fb64e8cfa6d2a81e8d712f58041b04975be0bd9" and "6d3832f6822b2e284d3baed7bbe047edba49bb7b" have entirely different histories.
0fb64e8cfa
...
6d3832f682
|
|
@ -1,119 +0,0 @@
|
||||||
# رفع مشکل کدهای تکراری حسابداری
|
|
||||||
|
|
||||||
## مشکل
|
|
||||||
سیستم حسابداری گاهی اوقات کدهای تکراری به اسناد میداد که باعث خطا در سیستم میشد. این مشکل به دلایل زیر رخ میداد:
|
|
||||||
|
|
||||||
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**: عملکرد سیستم را پس از اعمال تغییرات نظارت کنید
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
# رفع خطای 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` را به طور کامل حل میکند و سیستم را در برابر کدهای تکراری محافظت میکند.
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
# راهنمای کدهای عددی حسابداری
|
|
||||||
|
|
||||||
## اصل کلی
|
|
||||||
**تمام کدهای اسناد حسابداری باید فقط عدد باشند و هیچ حرفی نداشته باشند.**
|
|
||||||
|
|
||||||
## تغییرات اعمال شده
|
|
||||||
|
|
||||||
### 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 کدهای تکراری موجود را پیدا کرده و با کدهای عددی منحصر به فرد جایگزین میکند.
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
# تولید کدهای معقول حسابداری
|
|
||||||
|
|
||||||
## مشکل
|
|
||||||
کدهای تولید شده خیلی بلند و خارج از عرف حسابداری بودند (مثل `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"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
این تغییرات کدهای حسابداری را مطابق با عرف و استانداردهای حسابداری میکند.
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
# پیدا کردن ایمن 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,7 +52,6 @@ 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;
|
||||||
|
|
||||||
|
|
@ -64,8 +63,8 @@ class HesabdariController extends AbstractController
|
||||||
'money' => $acc['money']
|
'money' => $acc['money']
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
// Default: only approved documents - استفاده از متد ایمن
|
// Default: only approved documents
|
||||||
$doc = $provider->findHesabdariDocSafely($entityManager, [
|
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
||||||
'bid' => $acc['bid'],
|
'bid' => $acc['bid'],
|
||||||
'year' => $acc['year'],
|
'year' => $acc['year'],
|
||||||
'code' => $params['code'],
|
'code' => $params['code'],
|
||||||
|
|
@ -440,11 +439,6 @@ 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'],
|
||||||
|
|
@ -479,17 +473,7 @@ 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'];
|
||||||
|
|
@ -647,11 +631,7 @@ class HesabdariController extends AbstractController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ثبت تراکنش
|
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
$entityManager->commit();
|
|
||||||
|
|
||||||
$log->insert(
|
$log->insert(
|
||||||
'حسابداری',
|
'حسابداری',
|
||||||
'سند حسابداری شماره ' . $doc->getCode() . ' ثبت / ویرایش شد.',
|
'سند حسابداری شماره ' . $doc->getCode() . ' ثبت / ویرایش شد.',
|
||||||
|
|
@ -664,20 +644,10 @@ 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, Provider $provider): JsonResponse
|
public function app_accounting_remove_doc(Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
|
||||||
{
|
{
|
||||||
$params = [];
|
$params = [];
|
||||||
if ($content = $request->getContent()) {
|
if ($content = $request->getContent()) {
|
||||||
|
|
@ -685,8 +655,7 @@ 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')
|
||||||
]);
|
]);
|
||||||
|
|
@ -1389,69 +1358,4 @@ 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,415 +81,19 @@ 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();
|
||||||
$this->entityManager->commit();
|
return $count;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
<?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