progress in close year

This commit is contained in:
Hesabix 2025-08-12 21:58:06 +00:00
parent cd6821969f
commit 5afd0236c6
10 changed files with 2706 additions and 146 deletions

View file

@ -5,6 +5,22 @@ parameters:
sealDir: '%kernel.project_dir%/../hesabixArchive/seal'
SupportFilesDir: '%kernel.project_dir%/../hesabixArchive/support'
# تنظیمات سیستم بستن سال مالی
close_year.accounts.profit_loss: '999999'
close_year.accounts.retained_earnings: '999998'
close_year.account_types.temporary: ['calc'] # حساب‌های موقت (درآمد و هزینه)
close_year.account_types.permanent: ['calc'] # حساب‌های دائمی (دارایی، بدهی، سرمایه)
close_year.defaults.tax_percent: 0
close_year.defaults.dividend_percent: 0
close_year.defaults.new_year_duration: 31563000
close_year.backup.enabled: true
close_year.backup.directory: '%kernel.project_dir%/var/backups/'
close_year.logging.enabled: true
close_year.logging.level: 'info'
close_year.security.required_role: 'plugAccproCloseYear'
close_year.security.max_retry_attempts: 3
close_year.security.transaction_timeout: 300
services:
_defaults:
autowire: true
@ -167,3 +183,11 @@ services:
$jdate: '@Jdate'
$sms: '@SMS'
$uploadDirectory: '%SupportFilesDir%'
# سرویس بستن سال مالی
App\Service\CloseYearService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$logService: '@Log'
$provider: '@Provider'
$params: '@parameter_bag'

View file

@ -53,6 +53,7 @@ class AccountingDocService
return ['error' => 'شخص یافت نشد'];
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'person' => $person,
'year' => $acc['year']
], [
'id' => 'DESC'
]);
@ -65,6 +66,7 @@ class AccountingDocService
return ['error' => 'بانک یافت نشد'];
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'bank' => $bank,
'year' => $acc['year']
], [
'id' => 'DESC'
]);
@ -77,6 +79,7 @@ class AccountingDocService
return ['error' => 'صندوق یافت نشد'];
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'cashdesk' => $cashdesk,
'year' => $acc['year']
], [
'id' => 'DESC'
]);
@ -89,6 +92,7 @@ class AccountingDocService
return ['error' => 'حقوق یافت نشد'];
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'salary' => $salary,
'year' => $acc['year']
], [
'id' => 'DESC'
]);

View file

@ -0,0 +1,433 @@
<?php
namespace App\Controller;
use App\Entity\BankAccount;
use App\Entity\Business;
use App\Entity\Cashdesk;
use App\Entity\HesabdariDoc;
use App\Entity\HesabdariRow;
use App\Entity\HesabdariTable;
use App\Entity\Log as EntityLog;
use App\Entity\Money;
use App\Entity\Person;
use App\Entity\Salary;
use App\Entity\Year;
use App\Service\Access;
use App\Service\CloseYearService;
use App\Service\Explore;
use App\Service\Jdate;
use App\Service\Log;
use App\Service\Provider;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class CloseYearController extends AbstractController
{
private CloseYearService $closeYearService;
public function __construct(CloseYearService $closeYearService)
{
$this->closeYearService = $closeYearService;
}
#[Route('/api/year/close/prepare', name: 'app_year_close_prepare')]
public function app_year_close_prepare(Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('plugAccproCloseYear');
if (!$acc)
throw $this->createAccessDeniedException();
$currentYear = $entityManager->getRepository(Year::class)->findOneBy([
'bid' => $acc['bid'],
'head' => true
]);
if (!$currentYear) {
return $this->json([
'result' => 0,
'msg' => 'سال مالی فعال یافت نشد'
]);
}
// محاسبه سود و زیان
$profitLoss = $this->closeYearService->calculateProfitLoss($currentYear);
// محاسبه ترازنامه
$balanceSheet = $this->closeYearService->calculateBalanceSheet($currentYear);
// دریافت ساختار درختی حساب‌ها
$accountsTree = $this->getAccountsTree($entityManager, $acc['bid'], $currentYear);
return $this->json([
'result' => 1,
'currentYear' => [
'id' => $currentYear->getId(),
'label' => $currentYear->getLabel(),
'start' => $currentYear->getStart(),
'end' => $currentYear->getEnd()
],
'profitLoss' => $profitLoss,
'balanceSheet' => $balanceSheet,
'accountsTree' => $accountsTree
]);
}
#[Route('/api/year/close/execute', name: 'app_year_close_execute', methods: ['POST'])]
public function app_year_close_execute(Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, Jdate $jdate, Provider $provider): JsonResponse
{
$acc = $access->hasRole('plugAccproCloseYear');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$currentYear = $entityManager->getRepository(Year::class)->findOneBy([
'bid' => $acc['bid'],
'head' => true
]);
if (!$currentYear) {
return $this->json([
'result' => 0,
'msg' => 'سال مالی فعال یافت نشد'
]);
}
try {
$entityManager->beginTransaction();
// بستن حساب‌های موقت
$this->closeYearService->closeTemporaryAccounts($currentYear, $params, $this->getUser());
// ایجاد سال مالی جدید
$newYear = $this->closeYearService->createNewYear($acc['bid'], $params);
// انتقال مانده حساب‌های دائمی
$this->closeYearService->transferPermanentAccounts($currentYear, $newYear, $this->getUser());
// ثبت سود انباشته
$this->closeYearService->recordRetainedEarnings($currentYear, $newYear, $params, $this->getUser());
// تغییر وضعیت سال مالی
$currentYear->setHead(false);
$newYear->setHead(true);
$entityManager->persist($currentYear);
$entityManager->persist($newYear);
$entityManager->flush();
$entityManager->commit();
$log->insert(
'بستن سال مالی',
'سال مالی ' . $currentYear->getLabel() . ' بسته شد و سال مالی جدید ' . $newYear->getLabel() . ' ایجاد شد.',
$this->getUser(),
$request->headers->get('activeBid'),
null
);
return $this->json([
'result' => 1,
'msg' => 'سال مالی با موفقیت بسته شد',
'newYear' => [
'id' => $newYear->getId(),
'label' => $newYear->getLabel()
]
]);
} catch (\Exception $e) {
$entityManager->rollback();
return $this->json([
'result' => 0,
'msg' => 'خطا در بستن سال مالی: ' . $e->getMessage()
]);
}
}
#[Route('/api/year/close/validate', name: 'app_year_close_validate', methods: ['POST'])]
public function app_year_close_validate(Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('plugAccproCloseYear');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
try {
// اعتبارسنجی تاریخ‌های سال مالی جدید
$this->closeYearService->validateNewYearDates($acc['bid'], $params);
return $this->json([
'result' => 1,
'msg' => 'تاریخ‌های سال مالی معتبر است',
'error' => false
]);
} catch (\InvalidArgumentException $e) {
return $this->json([
'result' => 0,
'msg' => $e->getMessage(),
'error' => true
]);
}
}
#[Route('/api/year/close/accounts', name: 'app_year_close_accounts')]
public function app_year_close_accounts(Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('plugAccproCloseYear');
if (!$acc)
throw $this->createAccessDeniedException();
$currentYear = $entityManager->getRepository(Year::class)->findOneBy([
'bid' => $acc['bid'],
'head' => true
]);
if (!$currentYear) {
return $this->json([
'result' => 0,
'msg' => 'سال مالی فعال یافت نشد'
]);
}
// دریافت تمام حساب‌های حسابداری به صورت درختی
$accounts = $this->getAccountsTree($entityManager, $acc['bid'], $currentYear);
return $this->json([
'result' => 1,
'accounts' => $accounts
]);
}
/**
* دریافت ساختار درختی حساب‌های حسابداری
*/
private function getAccountsTree(EntityManagerInterface $entityManager, Business $business, Year $year): array
{
$accountsTree = [];
// دریافت حساب‌های عمومی اصلی (بدون parent)
$publicMainAccounts = $entityManager->getRepository(HesabdariTable::class)->findBy([
'bid' => null,
'upper' => null
]);
foreach ($publicMainAccounts as $mainAccount) {
$accountData = $this->buildAccountTreeData($entityManager, $mainAccount, $year, $business);
if ($accountData) {
$accountData['isPublic'] = true;
$accountsTree[] = $accountData;
}
}
// دریافت حساب‌های اختصاصی اصلی (بدون parent)
$privateMainAccounts = $entityManager->getRepository(HesabdariTable::class)->findBy([
'bid' => $business,
'upper' => null
]);
foreach ($privateMainAccounts as $mainAccount) {
$accountData = $this->buildAccountTreeData($entityManager, $mainAccount, $year, $business);
if ($accountData) {
$accountData['isPublic'] = false;
$accountsTree[] = $accountData;
}
}
return $accountsTree;
}
/**
* ساخت داده‌های درختی حساب
*/
private function buildAccountTreeData(EntityManagerInterface $entityManager, HesabdariTable $account, Year $year, Business $business): ?array
{
// محاسبه مانده کل حساب و زیرمجموعه‌های آن
$totalBalance = $this->calculateAccountTreeBalance($entityManager, $account, $year, $business);
// محاسبه مانده با در نظر گرفتن نوع حساب
$balanceWithType = $this->calculateBalanceWithType($account->getCode(), $totalBalance);
// اگر مانده صفر است و زیرمجموعه‌ای ندارد، نمایش نده
if ($balanceWithType == 0 && !$this->hasChildren($entityManager, $account, $business)) {
return null;
}
$accountData = [
'id' => $account->getId(),
'name' => $account->getName(),
'code' => $account->getCode(),
'type' => $account->getType(),
'balance' => $balanceWithType,
'children' => []
];
// دریافت زیرمجموعه‌ها (عمومی و اختصاصی)
$childAccounts = $this->getChildAccounts($entityManager, $account, $business);
foreach ($childAccounts as $childAccount) {
$childData = $this->buildAccountTreeData($entityManager, $childAccount, $year, $business);
if ($childData) {
$childData['isPublic'] = $childAccount->getBid() === null;
$accountData['children'][] = $childData;
}
}
return $accountData;
}
/**
* محاسبه مانده کل یک حساب و تمام زیرمجموعه‌های آن
*/
private function calculateAccountTreeBalance(EntityManagerInterface $entityManager, HesabdariTable $account, Year $year, Business $business): float
{
$totalBalance = 0;
// محاسبه مانده خود حساب
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy([
'ref' => $account,
'year' => $year
]);
foreach ($rows as $row) {
// محاسبه مانده بر اساس کد حساب به جای type
$totalBalance += (float)$row->getBd() - (float)$row->getBs();
}
// محاسبه مانده تمام زیرمجموعه‌ها (عمومی و اختصاصی)
$childAccounts = $this->getChildAccounts($entityManager, $account, $business);
foreach ($childAccounts as $childAccount) {
$totalBalance += $this->calculateAccountTreeBalance($entityManager, $childAccount, $year, $business);
}
return $totalBalance;
}
/**
* محاسبه مانده با در نظر گرفتن نوع حساب
*/
private function calculateBalanceWithType(string $code, float $balance): float
{
if ($this->isDebitAccount($code)) {
return $balance; // بدهکار: بدهی - بستانکاری
} else {
return -$balance; // بستانکار: بستانکاری - بدهی
}
}
/**
* بررسی اینکه آیا حساب بدهکار است یا بستانکار
*/
private function isDebitAccount(string $code): bool
{
$codeInt = (int)$code;
// دارایی‌ها (کدهای 2-19، به جز 8 و 9): بدهکار
if ($codeInt >= 2 && $codeInt <= 19 && $codeInt != 8 && $codeInt != 9) {
return true;
}
// موجودی کالا (کد 120): بدهکار
if ($codeInt == 120) {
return true;
}
// حساب‌های کنترلی (کد 117): بدهکار
if ($codeInt == 117) {
return true;
}
// هزینه‌ها (کدهای 67-111): بدهکار
if ($codeInt >= 67 && $codeInt <= 111) {
return true;
}
// بدهی‌ها (کدهای 6-39): بستانکار
if ($codeInt >= 6 && $codeInt <= 39) {
return false;
}
// سرمایه (کدهای 40-47): بستانکار
if ($codeInt >= 40 && $codeInt <= 47) {
return false;
}
// درآمدها (کدهای 56-66): بستانکار
if ($codeInt >= 56 && $codeInt <= 66) {
return false;
}
// فروش (کدهای 52-55): بستانکار
if ($codeInt >= 52 && $codeInt <= 55) {
return false;
}
// بهای تمام شده (کدهای 48-51): بدهکار
if ($codeInt >= 48 && $codeInt <= 51) {
return true;
}
// سایر حساب‌ها: پیش‌فرض بستانکار
return false;
}
/**
* دریافت زیرمجموعه‌های یک حساب (عمومی و اختصاصی)
*/
private function getChildAccounts(EntityManagerInterface $entityManager, HesabdariTable $parentAccount, Business $business): array
{
$childAccounts = [];
// دریافت زیرمجموعه‌های عمومی
$publicChildren = $entityManager->getRepository(HesabdariTable::class)->findBy([
'upper' => $parentAccount,
'bid' => null
]);
foreach ($publicChildren as $child) {
$childAccounts[] = $child;
}
// دریافت زیرمجموعه‌های اختصاصی
$privateChildren = $entityManager->getRepository(HesabdariTable::class)->findBy([
'upper' => $parentAccount,
'bid' => $business
]);
foreach ($privateChildren as $child) {
$childAccounts[] = $child;
}
return $childAccounts;
}
/**
* بررسی وجود زیرمجموعه
*/
private function hasChildren(EntityManagerInterface $entityManager, HesabdariTable $account, Business $business): bool
{
// بررسی زیرمجموعه‌های عمومی
$publicChildCount = $entityManager->getRepository(HesabdariTable::class)->count([
'upper' => $account,
'bid' => null
]);
// بررسی زیرمجموعه‌های اختصاصی
$privateChildCount = $entityManager->getRepository(HesabdariTable::class)->count([
'upper' => $account,
'bid' => $business
]);
return ($publicChildCount + $privateChildCount) > 0;
}
}

View file

@ -621,7 +621,7 @@ class SellController extends AbstractController
if ($commodityId) {
$last = $entityManager->getRepository(HesabdariRow::class)
->findOneBy(['commodity' => $commodityId, 'bs' => 0], ['id' => 'DESC']);
if ($last) {
if ($last && $last->getCommdityCount() > 0 && $item->getCommdityCount() > 0) {
$price = $last->getBd() / $last->getCommdityCount();
$profit += ($item->getBs() / $item->getCommdityCount() - $price) * $item->getCommdityCount();
} else {
@ -646,7 +646,7 @@ class SellController extends AbstractController
$avg += $last->getBd();
$count += $last->getCommdityCount();
}
if ($count != 0) {
if ($count != 0 && $item->getCommdityCount() > 0) {
$price = $avg / $count;
$profit += ($item->getBs() / $item->getCommdityCount() - $price) * $item->getCommdityCount();
} else {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,99 @@
<?php
namespace App\Tests;
use App\Entity\Business;
use App\Entity\HesabdariTable;
use App\Entity\Year;
use App\Service\CloseYearService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class CloseYearAccountsTest extends KernelTestCase
{
private CloseYearService $closeYearService;
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->entityManager = $kernel->getContainer()->get('doctrine')->getManager();
$this->closeYearService = static::getContainer()->get(CloseYearService::class);
}
public function testEnsureCloseYearAccountsExist(): void
{
// حذف حساب‌های موجود (اگر وجود دارند)
$this->entityManager->createQueryBuilder()
->delete(HesabdariTable::class, 'h')
->where('h.code IN (:codes)')
->setParameter('codes', ['999998', '999999'])
->getQuery()
->execute();
// بررسی اینکه حساب‌ها وجود ندارند
$profitLossAccount = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '999999']);
$retainedEarningsAccount = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '999998']);
$this->assertNull($profitLossAccount);
$this->assertNull($retainedEarningsAccount);
// ایجاد یک سال مالی تست
$business = new Business();
$business->setName('Test Business');
$this->entityManager->persist($business);
$year = new Year();
$year->setBid($business);
$year->setLabel('سال تست');
$year->setStart(time());
$year->setEnd(time() + 31536000);
$year->setHead(true);
$this->entityManager->persist($year);
$this->entityManager->flush();
// اجرای بستن حساب‌های موقت (که خودکار حساب‌ها را ایجاد می‌کند)
$this->closeYearService->closeTemporaryAccounts($year, []);
// بررسی اینکه حساب‌ها ایجاد شده‌اند
$profitLossAccount = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '999999']);
$retainedEarningsAccount = $this->entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '999998']);
$this->assertNotNull($profitLossAccount);
$this->assertNotNull($retainedEarningsAccount);
$this->assertEquals('سود و زیان', $profitLossAccount->getName());
$this->assertEquals('سود انباشته', $retainedEarningsAccount->getName());
$this->assertNull($profitLossAccount->getBid()); // حساب عمومی
$this->assertNull($retainedEarningsAccount->getBid()); // حساب عمومی
// پاکسازی
$this->entityManager->remove($year);
$this->entityManager->remove($business);
$this->entityManager->flush();
}
public function testGetOrCreateAccount(): void
{
// تست ایجاد حساب جدید
$business = new Business();
$business->setName('Test Business');
$this->entityManager->persist($business);
$account = $this->closeYearService->getOrCreateAccount($business, '999999', 'سود و زیان', 'calc');
$this->assertNotNull($account);
$this->assertEquals('999999', $account->getCode());
$this->assertEquals('سود و زیان', $account->getName());
$this->assertEquals('calc', $account->getType());
$this->assertNull($account->getBid()); // حساب عمومی
// تست دریافت حساب موجود
$existingAccount = $this->closeYearService->getOrCreateAccount($business, '999999', 'سود و زیان', 'calc');
$this->assertEquals($account->getId(), $existingAccount->getId());
// پاکسازی
$this->entityManager->remove($business);
$this->entityManager->flush();
}
}

View file

@ -0,0 +1,159 @@
<?php
namespace App\Tests;
use App\Entity\Business;
use App\Entity\HesabdariTable;
use App\Entity\HesabdariRow;
use App\Entity\Year;
use App\Entity\User;
use App\Entity\Money;
use App\Service\CloseYearService;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class CloseYearTest extends KernelTestCase
{
private CloseYearService $closeYearService;
protected function setUp(): void
{
self::bootKernel();
$this->closeYearService = static::getContainer()->get(CloseYearService::class);
}
public function testIsDebitAccount(): void
{
// تست حساب‌های دارایی (باید بدهکار باشند)
$this->assertTrue($this->closeYearService->isDebitAccount('2')); // دارایی‌های جاری
$this->assertTrue($this->closeYearService->isDebitAccount('3')); // حساب‌های دریافتی
$this->assertTrue($this->closeYearService->isDebitAccount('4')); // موجودی نقد و بانک
$this->assertTrue($this->closeYearService->isDebitAccount('5')); // حساب‌های بانکی
$this->assertTrue($this->closeYearService->isDebitAccount('10')); // دارایی‌های غیر جاری
$this->assertTrue($this->closeYearService->isDebitAccount('11')); // دارایی‌های ثابت
$this->assertTrue($this->closeYearService->isDebitAccount('12')); // زمین
$this->assertTrue($this->closeYearService->isDebitAccount('13')); // ساختمان
$this->assertTrue($this->closeYearService->isDebitAccount('14')); // وسائل نقلیه
$this->assertTrue($this->closeYearService->isDebitAccount('15')); // اثاثیه اداری
$this->assertTrue($this->closeYearService->isDebitAccount('16')); // استهلاک انباشته
$this->assertTrue($this->closeYearService->isDebitAccount('17')); // استهلاک انباشته ساختمان
$this->assertTrue($this->closeYearService->isDebitAccount('18')); // استهلاک انباشته وسائل نقلیه
$this->assertTrue($this->closeYearService->isDebitAccount('19')); // استهلاک انباشته اثاثیه اداری
// تست حساب‌های بدهی (باید بستانکار باشند)
$this->assertFalse($this->closeYearService->isDebitAccount('6')); // بدهی‌های جاری
$this->assertFalse($this->closeYearService->isDebitAccount('7')); // حساب ها و اسناد پرداختنی
$this->assertFalse($this->closeYearService->isDebitAccount('8')); // اسناد پرداختنی
$this->assertFalse($this->closeYearService->isDebitAccount('9')); // حساب‌های پرداختنی
$this->assertFalse($this->closeYearService->isDebitAccount('22')); // سایر حساب های پرداختنی
$this->assertFalse($this->closeYearService->isDebitAccount('23')); // ذخیره مالیات بر درآمد پرداختنی
$this->assertFalse($this->closeYearService->isDebitAccount('24')); // مالیات بر درآمد پرداختنی
$this->assertFalse($this->closeYearService->isDebitAccount('25')); // مالیات حقوق و دستمزد پرداختنی
$this->assertFalse($this->closeYearService->isDebitAccount('26')); // حق بیمه پرداختنی
$this->assertFalse($this->closeYearService->isDebitAccount('27')); // حقوق و دستمزد پرداختنی
$this->assertFalse($this->closeYearService->isDebitAccount('28')); // عیدی و پاداش پرداختنی
$this->assertFalse($this->closeYearService->isDebitAccount('29')); // سایر هزینه های پرداختنی
$this->assertFalse($this->closeYearService->isDebitAccount('30')); // پیش دریافت ها
$this->assertFalse($this->closeYearService->isDebitAccount('31')); // پیش دریافت فروش
$this->assertFalse($this->closeYearService->isDebitAccount('32')); // سایر پیش دریافت ها
$this->assertFalse($this->closeYearService->isDebitAccount('33')); // مالیات بر ارزش افزوده فروش
$this->assertFalse($this->closeYearService->isDebitAccount('34')); // بدهیهای غیر جاری
$this->assertFalse($this->closeYearService->isDebitAccount('35')); // حساب ها و اسناد پرداختنی بلندمدت
$this->assertFalse($this->closeYearService->isDebitAccount('36')); // حساب های پرداختنی بلندمدت
$this->assertFalse($this->closeYearService->isDebitAccount('37')); // اسناد پرداختنی بلندمدت
$this->assertFalse($this->closeYearService->isDebitAccount('38')); // ذخیره مزایای پایان خدمت کارکنان
$this->assertFalse($this->closeYearService->isDebitAccount('39')); // وام پرداختنی
// تست حساب‌های سرمایه (باید بستانکار باشند)
$this->assertFalse($this->closeYearService->isDebitAccount('40')); // حقوق صاحبان سهام
$this->assertFalse($this->closeYearService->isDebitAccount('41')); // سرمایه
$this->assertFalse($this->closeYearService->isDebitAccount('42')); // سرمایه اولیه
$this->assertFalse($this->closeYearService->isDebitAccount('43')); // افزایش یا کاهش سرمایه
$this->assertFalse($this->closeYearService->isDebitAccount('44')); // اندوخته قانونی
$this->assertFalse($this->closeYearService->isDebitAccount('45')); // برداشت ها
$this->assertFalse($this->closeYearService->isDebitAccount('46')); // سهم سود و زیان
$this->assertFalse($this->closeYearService->isDebitAccount('47')); // سود یا زیان انباشته
// تست حساب‌های بهای تمام شده (باید بدهکار باشند)
$this->assertTrue($this->closeYearService->isDebitAccount('48')); // بهای تمام شده کالای فروخته شده
$this->assertTrue($this->closeYearService->isDebitAccount('49')); // بهای تمام شده کالای فروخته شده
$this->assertTrue($this->closeYearService->isDebitAccount('50')); // برگشت از خرید
$this->assertTrue($this->closeYearService->isDebitAccount('51')); // تخفیفات نقدی خرید
// تست حساب‌های فروش (باید بستانکار باشند)
$this->assertFalse($this->closeYearService->isDebitAccount('52')); // فروش
$this->assertFalse($this->closeYearService->isDebitAccount('53')); // فروش کالا
$this->assertFalse($this->closeYearService->isDebitAccount('54')); // برگشت از فروش
$this->assertFalse($this->closeYearService->isDebitAccount('55')); // تخفیفات نقدی فروش
// تست حساب‌های درآمد (باید بستانکار باشند)
$this->assertFalse($this->closeYearService->isDebitAccount('56')); // درآمد
$this->assertFalse($this->closeYearService->isDebitAccount('57')); // درآمد های عملیاتی
$this->assertFalse($this->closeYearService->isDebitAccount('58')); // درآمد حاصل از فروش خدمات
$this->assertFalse($this->closeYearService->isDebitAccount('59')); // برگشت از خرید خدمات
$this->assertFalse($this->closeYearService->isDebitAccount('60')); // درآمد اضافه کالا
$this->assertFalse($this->closeYearService->isDebitAccount('61')); // درآمد حمل کالا
$this->assertFalse($this->closeYearService->isDebitAccount('62')); // درآمد های غیر عملیاتی
$this->assertFalse($this->closeYearService->isDebitAccount('63')); // درآمد حاصل از سرمایه گذاری
$this->assertFalse($this->closeYearService->isDebitAccount('64')); // درآمد سود سپرده ها
$this->assertFalse($this->closeYearService->isDebitAccount('65')); // سایر درآمد ها
$this->assertFalse($this->closeYearService->isDebitAccount('66')); // درآمد تسعیر ارز
// تست حساب‌های هزینه (باید بدهکار باشند)
$this->assertTrue($this->closeYearService->isDebitAccount('67')); // هزینه ها
$this->assertTrue($this->closeYearService->isDebitAccount('68')); // هزینه های پرسنلی
$this->assertTrue($this->closeYearService->isDebitAccount('69')); // هزینه حقوق و دستمزد
$this->assertTrue($this->closeYearService->isDebitAccount('70')); // حقوق پایه
$this->assertTrue($this->closeYearService->isDebitAccount('71')); // اضافه کار
$this->assertTrue($this->closeYearService->isDebitAccount('72')); // حق شیفت و شب کاری
$this->assertTrue($this->closeYearService->isDebitAccount('73')); // حق نوبت کاری
$this->assertTrue($this->closeYearService->isDebitAccount('74')); // حق ماموریت
$this->assertTrue($this->closeYearService->isDebitAccount('75')); // فوق العاده مسکن و خاروبار
$this->assertTrue($this->closeYearService->isDebitAccount('76')); // حق اولاد
$this->assertTrue($this->closeYearService->isDebitAccount('77')); // عیدی و پاداش
$this->assertTrue($this->closeYearService->isDebitAccount('78')); // بازخرید سنوات خدمت کارکنان
$this->assertTrue($this->closeYearService->isDebitAccount('79')); // بازخرید مرخصی
$this->assertTrue($this->closeYearService->isDebitAccount('80')); // بیمه سهم کارفرما
$this->assertTrue($this->closeYearService->isDebitAccount('81')); // بیمه بیکاری
$this->assertTrue($this->closeYearService->isDebitAccount('82')); // حقوق مزایای متفرقه
$this->assertTrue($this->closeYearService->isDebitAccount('83')); // سایر هزینه های کارکنان
$this->assertTrue($this->closeYearService->isDebitAccount('84')); // سفر و ماموریت
$this->assertTrue($this->closeYearService->isDebitAccount('85')); // ایاب و ذهاب
$this->assertTrue($this->closeYearService->isDebitAccount('86')); // سایر هزینه های کارکنان
$this->assertTrue($this->closeYearService->isDebitAccount('87')); // هزینه های عملیاتی
$this->assertTrue($this->closeYearService->isDebitAccount('88')); // خرید خدمات
$this->assertTrue($this->closeYearService->isDebitAccount('89')); // برگشت از فروش خدمات
$this->assertTrue($this->closeYearService->isDebitAccount('90')); // هزینه حمل کالا
$this->assertTrue($this->closeYearService->isDebitAccount('91')); // تعمیر و نگهداری اموال و اثاثیه
$this->assertTrue($this->closeYearService->isDebitAccount('92')); // هزینه اجاره محل
$this->assertTrue($this->closeYearService->isDebitAccount('93')); // هزینه های عمومی
$this->assertTrue($this->closeYearService->isDebitAccount('94')); // هزینه ملزومات مصرفی
$this->assertTrue($this->closeYearService->isDebitAccount('95')); // هزینه کسری و ضایعات کالا
$this->assertTrue($this->closeYearService->isDebitAccount('96')); // بیمه دارایی های ثابت
$this->assertTrue($this->closeYearService->isDebitAccount('97')); // هزینه های استهلاک
$this->assertTrue($this->closeYearService->isDebitAccount('98')); // هزینه استهلاک ساختمان
$this->assertTrue($this->closeYearService->isDebitAccount('99')); // هزینه استهلاک وسائل نقلیه
$this->assertTrue($this->closeYearService->isDebitAccount('100')); // هزینه استهلاک اثاثیه
$this->assertTrue($this->closeYearService->isDebitAccount('101')); // هزینه های بازاریابی و توزیع و فروش
$this->assertTrue($this->closeYearService->isDebitAccount('102')); // هزینه آگهی و تبلیغات
$this->assertTrue($this->closeYearService->isDebitAccount('103')); // هزینه بازاریابی و پورسانت
$this->assertTrue($this->closeYearService->isDebitAccount('104')); // سایر هزینه های توزیع و فروش
$this->assertTrue($this->closeYearService->isDebitAccount('105')); // هزینه های غیرعملیاتی
$this->assertTrue($this->closeYearService->isDebitAccount('106')); // هزینه های بانکی
$this->assertTrue($this->closeYearService->isDebitAccount('107')); // سود و کارمزد وامها
$this->assertTrue($this->closeYearService->isDebitAccount('108')); // کارمزد خدمات بانکی
$this->assertTrue($this->closeYearService->isDebitAccount('109')); // جرائم دیرکرد بانکی
$this->assertTrue($this->closeYearService->isDebitAccount('110')); // هزینه تسعیر ارز
$this->assertTrue($this->closeYearService->isDebitAccount('111')); // هزینه مطالبات سوخت شده
}
public function testRouteExists(): void
{
$client = static::createClient();
$router = static::getContainer()->get('router');
// بررسی وجود route ها
$this->assertNotNull($router->getRouteCollection()->get('app_year_close_prepare'));
$this->assertNotNull($router->getRouteCollection()->get('app_year_close_execute'));
$this->assertNotNull($router->getRouteCollection()->get('app_year_close_accounts'));
}
}

View file

@ -0,0 +1,207 @@
<?php
namespace App\Tests\Controller;
use App\Entity\Business;
use App\Entity\HesabdariTable;
use App\Entity\HesabdariRow;
use App\Entity\Year;
use App\Entity\User;
use App\Entity\Money;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;
class CloseYearControllerTest extends WebTestCase
{
private $client;
private $entityManager;
protected function setUp(): void
{
$this->client = static::createClient();
$this->entityManager = static::getContainer()->get('doctrine')->getManager();
}
public function testPrepareCloseYear(): void
{
// ایجاد داده‌های تست
$business = $this->createTestBusiness();
$year = $this->createTestYear($business);
$user = $this->createTestUser($business);
// شبیه‌سازی احراز هویت
$this->client->request('POST', '/api/year/close/prepare', [], [], [
'HTTP_Authorization' => 'Bearer test-token',
'HTTP_activeBid' => $business->getId(),
'HTTP_activeYear' => $year->getId()
]);
$this->assertResponseIsSuccessful();
$response = json_decode($this->client->getResponse()->getContent(), true);
$this->assertEquals(1, $response['result']);
$this->assertArrayHasKey('currentYear', $response);
$this->assertArrayHasKey('profitLoss', $response);
$this->assertArrayHasKey('balanceSheet', $response);
}
public function testExecuteCloseYear(): void
{
// ایجاد داده‌های تست
$business = $this->createTestBusiness();
$year = $this->createTestYear($business);
$user = $this->createTestUser($business);
$closeData = [
'taxPercent' => 25.0,
'dividendPercent' => 50.0,
'newYearLabel' => 'سال مالی تست',
'newYearStart' => '1405/01/01',
'newYearEnd' => '1406/01/01'
];
// شبیه‌سازی احراز هویت
$this->client->request('POST', '/api/year/close/execute', [], [], [
'HTTP_Authorization' => 'Bearer test-token',
'HTTP_activeBid' => $business->getId(),
'HTTP_activeYear' => $year->getId()
], json_encode($closeData));
$this->assertResponseIsSuccessful();
$response = json_decode($this->client->getResponse()->getContent(), true);
$this->assertEquals(1, $response['result']);
$this->assertArrayHasKey('newYear', $response);
}
public function testGetAccounts(): void
{
// ایجاد داده‌های تست
$business = $this->createTestBusiness();
$year = $this->createTestYear($business);
$user = $this->createTestUser($business);
$this->createTestAccounts($business, $year);
// شبیه‌سازی احراز هویت
$this->client->request('GET', '/api/year/close/accounts', [], [], [
'HTTP_Authorization' => 'Bearer test-token',
'HTTP_activeBid' => $business->getId(),
'HTTP_activeYear' => $year->getId()
]);
$this->assertResponseIsSuccessful();
$response = json_decode($this->client->getResponse()->getContent(), true);
$this->assertEquals(1, $response['result']);
$this->assertArrayHasKey('accounts', $response);
$this->assertIsArray($response['accounts']);
}
private function createTestBusiness(): Business
{
$business = new Business();
$business->setName('شرکت تست');
$business->setCode('TEST001');
$this->entityManager->persist($business);
$this->entityManager->flush();
return $business;
}
private function createTestYear(Business $business): Year
{
$year = new Year();
$year->setBid($business);
$year->setLabel('سال مالی تست');
$year->setHead(true);
$year->setStart('1404/01/01');
$year->setEnd('1405/01/01');
$this->entityManager->persist($year);
$this->entityManager->flush();
return $year;
}
private function createTestUser(Business $business): User
{
$user = new User();
$user->setUsername('testuser');
$user->setEmail('test@example.com');
$user->setPassword('password');
$this->entityManager->persist($user);
$this->entityManager->flush();
return $user;
}
private function createTestAccounts(Business $business, Year $year): void
{
// ایجاد حساب درآمد
$incomeAccount = new HesabdariTable();
$incomeAccount->setBid($business);
$incomeAccount->setName('فروش');
$incomeAccount->setCode('4001');
$incomeAccount->setType('income');
$this->entityManager->persist($incomeAccount);
// ایجاد حساب هزینه
$expenseAccount = new HesabdariTable();
$expenseAccount->setBid($business);
$expenseAccount->setName('هزینه‌های عملیاتی');
$expenseAccount->setCode('5001');
$expenseAccount->setType('expense');
$this->entityManager->persist($expenseAccount);
// ایجاد حساب دارایی
$assetAccount = new HesabdariTable();
$assetAccount->setBid($business);
$assetAccount->setName('موجودی نقد');
$assetAccount->setCode('1001');
$assetAccount->setType('asset');
$this->entityManager->persist($assetAccount);
$this->entityManager->flush();
// ایجاد ردیف‌های حسابداری
$this->createTestRows($incomeAccount, $expenseAccount, $assetAccount, $year);
}
private function createTestRows(HesabdariTable $incomeAccount, HesabdariTable $expenseAccount, HesabdariTable $assetAccount, Year $year): void
{
// ردیف درآمد
$incomeRow = new HesabdariRow();
$incomeRow->setBid($year->getBid());
$incomeRow->setYear($year);
$incomeRow->setRef($incomeAccount);
$incomeRow->setBs('1000000');
$incomeRow->setBd('0');
$incomeRow->setDes('فروش دوره');
$this->entityManager->persist($incomeRow);
// ردیف هزینه
$expenseRow = new HesabdariRow();
$expenseRow->setBid($year->getBid());
$expenseRow->setYear($year);
$expenseRow->setRef($expenseAccount);
$expenseRow->setBs('0');
$expenseRow->setBd('600000');
$expenseRow->setDes('هزینه‌های دوره');
$this->entityManager->persist($expenseRow);
// ردیف دارایی
$assetRow = new HesabdariRow();
$assetRow->setBid($year->getBid());
$assetRow->setYear($year);
$assetRow->setRef($assetAccount);
$assetRow->setBs('0');
$assetRow->setBd('500000');
$assetRow->setDes('موجودی نقد');
$this->entityManager->persist($assetRow);
$this->entityManager->flush();
}
protected function tearDown(): void
{
parent::tearDown();
$this->entityManager->close();
$this->entityManager = null;
}
}

View file

@ -21,181 +21,730 @@
</div>
</div>
</div>
<h5 class="text-primary"> سال مالی :
{{ YearInfo.year.label }}
</h5>
<div class="row">
<div class="col-sm-12 col-md-6">
<div class="block block-rounded block-mode-loading-refresh">
<div class="block-header block-header-default bg-success text-light">
<h3 class="block-title">دارائیها</h3>
<div class="block-options">
<div class="col-sm-12">
<h5 class="text-primary"> سال مالی جاری:
{{ currentYear.label }}
</h5>
</div>
<!-- اطلاعات سود و زیان -->
<div class="col-sm-12 col-md-6">
<div class="block block-rounded block-mode-loading-refresh">
<div class="block-header block-header-default bg-info text-light">
<h3 class="block-title">سود و زیان</h3>
</div>
<div class="block-content p-3">
<div class="row">
<div class="col-6">
<strong>کل درآمد:</strong>
</div>
<div class="col-6 text-end">
{{ $filters.formatNumber(profitLoss.totalIncome) }} ریال
</div>
</div>
<div class="block-content p-0">
<table class="table table-striped table-hover table-borderless table-vcenter fs-sm text-center">
<thead>
<tr class="text-uppercase">
<th>آیتم</th>
<th>بستانکار</th>
<th>بدهکار</th>
<th>تراز</th>
<th>وضعیت</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class="fw-semibold">بانکها</span>
</td>
<td>
<span class="fs-sm text-muted">{{ $filters.formatNumber(YearInfo.banks.bd) }}</span>
</td>
<td>
<span class="fs-sm text-muted">{{ $filters.formatNumber(YearInfo.banks.bs) }}</span>
</td>
<td>
<span class="fs-sm text-muted">{{ $filters.formatNumber(Math.abs(YearInfo.banks.bs -
YearInfo.banks.bd)) }}</span>
</td>
<td>
<span v-if="(YearInfo.banks.bs - YearInfo.banks.bd) > 0"
class="fw-semibold text-warning">بدهکار</span>
<span v-if="(YearInfo.banks.bs - YearInfo.banks.bd) < 0"
class="fw-semibold text-success">بستانکار</span>
<span v-if="(YearInfo.banks.bs - YearInfo.banks.bd) == 0"
class="fw-semibold text-dark">تسویه</span>
</td>
</tr>
<tr>
<td>
<span class="fw-semibold">صندوقها</span>
</td>
<td>
<span class="fs-sm text-muted">{{ $filters.formatNumber(YearInfo.cashdesks.bd) }}</span>
</td>
<td>
<span class="fs-sm text-muted">{{ $filters.formatNumber(YearInfo.cashdesks.bs) }}</span>
</td>
<td>
<span class="fs-sm text-muted">{{ $filters.formatNumber(Math.abs(YearInfo.cashdesks.bd -
YearInfo.cashdesks.bs)) }}</span>
</td>
<td>
<span v-if="(YearInfo.cashdesks.bs - YearInfo.cashdesks.bd) > 0"
class="fw-semibold text-warning">بدهکار</span>
<span v-if="(YearInfo.cashdesks.bs - YearInfo.cashdesks.bd) < 0"
class="fw-semibold text-success">بستانکار</span>
<span v-if="(YearInfo.cashdesks.bs - YearInfo.cashdesks.bd) == 0"
class="fw-semibold text-dark">تسویه</span>
</td>
</tr>
<tr>
<td>
<span class="fw-semibold">تنخواه گردانها</span>
</td>
<td>
<span class="fs-sm text-muted">{{ $filters.formatNumber(YearInfo.salarys.bd) }}</span>
</td>
<td>
<span class="fs-sm text-muted">{{ $filters.formatNumber(YearInfo.salarys.bs) }}</span>
</td>
<td>
<span class="fs-sm text-muted">{{ $filters.formatNumber(Math.abs(YearInfo.salarys.bd -
YearInfo.salarys.bs)) }}</span>
</td>
<td>
<span v-if="(YearInfo.salarys.bs - YearInfo.salarys.bd) > 0"
class="fw-semibold text-warning">بدهکار</span>
<span v-if="(YearInfo.salarys.bs - YearInfo.salarys.bd) < 0"
class="fw-semibold text-success">بستانکار</span>
<span v-if="(YearInfo.salarys.bs - YearInfo.salarys.bd) == 0"
class="fw-semibold text-dark">تسویه</span>
</td>
</tr>
<tr>
<td>
<span class="fw-semibold">بدهکاران</span>
</td>
<td>
<span class="fs-sm text-muted">{{ $filters.formatNumber(YearInfo.persons.bd) }}</span>
</td>
<td>
<span class="fs-sm text-muted">{{ $filters.formatNumber(YearInfo.persons.bs) }}</span>
</td>
<td>
<span class="fs-sm text-muted">{{ $filters.formatNumber(Math.abs(YearInfo.persons.bs -
YearInfo.persons.bd)) }}</span>
</td>
<td>
<span v-if="(YearInfo.banks.bs - YearInfo.banks.bd) < 0"
class="fw-semibold text-warning">بدهکار</span>
<span v-if="(YearInfo.banks.bs - YearInfo.banks.bd) > 0"
class="fw-semibold text-success">بستانکار</span>
<span v-if="(YearInfo.banks.bs - YearInfo.banks.bd) == 0"
class="fw-semibold text-dark">تسویه</span>
</td>
</tr>
</tbody>
</table>
<div class="row">
<div class="col-6">
<strong>کل هزینه:</strong>
</div>
<div class="col-6 text-end">
{{ $filters.formatNumber(profitLoss.totalExpense) }} ریال
</div>
</div>
<hr>
<div class="row">
<div class="col-6">
<strong>سود خالص:</strong>
</div>
<div class="col-6 text-end" :class="profitLoss.netProfit >= 0 ? 'text-success' : 'text-danger'">
{{ $filters.formatNumber(profitLoss.netProfit) }} ریال
</div>
</div>
</div>
</div>
</div>
<!-- اطلاعات ترازنامه -->
<div class="col-sm-12 col-md-6">
<div class="block block-rounded block-mode-loading-refresh">
<div class="block-header block-header-default bg-success text-light">
<h3 class="block-title">ترازنامه</h3>
</div>
<div class="block-content p-3">
<div class="row">
<div class="col-6">
<strong>کل داراییها:</strong>
</div>
<div class="col-6 text-end">
{{ $filters.formatNumber(balanceSheet.totalAssets) }} ریال
</div>
</div>
<div class="row">
<div class="col-6">
<strong>کل بدهیها:</strong>
</div>
<div class="col-6 text-end">
{{ $filters.formatNumber(balanceSheet.totalLiabilities) }} ریال
</div>
</div>
<hr>
<div class="row">
<div class="col-6">
<strong>کل سرمایه:</strong>
</div>
<div class="col-6 text-end">
{{ $filters.formatNumber(balanceSheet.totalEquity) }} ریال
</div>
</div>
</div>
</div>
</div>
<!-- اطلاعات حسابهای حسابداری -->
<div class="col-sm-12">
<div class="block block-rounded">
<div class="block-header block-header-default bg-secondary text-light">
<h3 class="block-title">ساختار حسابهای حسابداری</h3>
</div>
<div class="block-content p-3">
<div class="row">
<div class="col-12">
<div class="account-tree">
<div v-for="account in accountsTree" :key="account.id" class="account-item">
<div class="account-header" :class="getAccountTypeClass(account.type)">
<span class="account-code">{{ account.code }}</span>
<span class="account-name">{{ account.name }}</span>
<span class="account-balance" :class="account.balance >= 0 ? 'text-success' : 'text-danger'">
{{ $filters.formatNumber(account.balance) }} ریال
</span>
<span class="account-type-badge" :class="account.isPublic ? 'badge bg-info' : 'badge bg-warning'">
{{ account.isPublic ? 'عمومی' : 'اختصاصی' }}
</span>
</div>
<div v-if="account.children && account.children.length > 0" class="account-children">
<div v-for="child in account.children" :key="child.id" class="account-item child">
<div class="account-header" :class="getAccountTypeClass(child.type)">
<span class="account-code">{{ child.code }}</span>
<span class="account-name">{{ child.name }}</span>
<span class="account-balance" :class="child.balance >= 0 ? 'text-success' : 'text-danger'">
{{ $filters.formatNumber(child.balance) }} ریال
</span>
<span class="account-type-badge" :class="child.isPublic ? 'badge bg-info' : 'badge bg-warning'">
{{ child.isPublic ? 'عمومی' : 'اختصاصی' }}
</span>
</div>
<div v-if="child.children && child.children.length > 0" class="account-children">
<div v-for="grandChild in child.children" :key="grandChild.id" class="account-item grandchild">
<div class="account-header" :class="getAccountTypeClass(grandChild.type)">
<span class="account-code">{{ grandChild.code }}</span>
<span class="account-name">{{ grandChild.name }}</span>
<span class="account-balance" :class="grandChild.balance >= 0 ? 'text-success' : 'text-danger'">
{{ $filters.formatNumber(grandChild.balance) }} ریال
</span>
<span class="account-type-badge" :class="grandChild.isPublic ? 'badge bg-info' : 'badge bg-warning'">
{{ grandChild.isPublic ? 'عمومی' : 'اختصاصی' }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- فرم بستن سال مالی -->
<div class="col-sm-12">
<div class="block block-rounded">
<div class="block-header block-header-default bg-primary text-light">
<h3 class="block-title">تنظیمات بستن سال مالی</h3>
</div>
<div class="block-content p-3">
<v-form @submit.prevent="closeYear" ref="form">
<!-- مالیات -->
<div class="row mb-3">
<div class="col-md-6">
<v-text-field
prepend-inner-icon="mdi-percent"
label="درصد مالیات بر درآمد"
v-model="closeData.taxPercent"
type="number"
min="0"
max="100"
step="0.01"
variant="outlined"
/>
</div>
<div class="col-md-6">
<v-text-field
prepend-inner-icon="mdi-cash"
label="مبلغ مالیات"
:model-value="$filters.formatNumber(taxAmount)"
readonly
variant="outlined"
/>
</div>
</div>
<!-- تقسیم سود -->
<div class="row mb-3">
<div class="col-md-6">
<v-text-field
prepend-inner-icon="mdi-percent"
label="درصد تقسیم سود"
v-model="closeData.dividendPercent"
type="number"
min="0"
max="100"
step="0.01"
variant="outlined"
/>
</div>
<div class="col-md-6">
<v-text-field
prepend-inner-icon="mdi-cash"
label="مبلغ تقسیم سود"
:model-value="$filters.formatNumber(dividendAmount)"
readonly
variant="outlined"
/>
</div>
</div>
<!-- سود انباشته -->
<div class="row mb-3">
<div class="col-md-6">
<v-text-field
prepend-inner-icon="mdi-cash-plus"
label="سود انباشته"
:model-value="$filters.formatNumber(retainedEarnings)"
readonly
variant="outlined"
color="success"
/>
</div>
</div>
<!-- اطلاعات سال مالی جدید -->
<h4 class="text-primary mb-3">اطلاعات سال مالی جدید</h4>
<div class="row mb-3">
<div class="col-md-4">
<v-text-field
prepend-inner-icon="mdi-text-box-outline"
:label="$t('pages.create_business.fiscal_year_label')"
:rules="[() => closeData.newYearLabel.length > 0 || $t('validator.required')]"
v-model="closeData.newYearLabel"
/>
</div>
<div class="col-md-4">
<v-text-field
prepend-inner-icon="mdi-calendar"
readonly
:rules="[() => closeData.newYearStart.length > 0 || $t('validator.required')]"
:label="$t('pages.create_business.fiscal_year_start')"
v-model="closeData.newYearStart"
class="txt_calendar_start"
/>
<CustomDatePicker
custom-input=".txt_calendar_start"
append-to="body"
v-model="closeData.newYearStart"
/>
</div>
<div class="col-md-4">
<v-text-field
prepend-inner-icon="mdi-calendar"
readonly
:rules="[() => closeData.newYearEnd.length > 0 || $t('validator.required')]"
:label="$t('pages.create_business.fiscal_year_end')"
v-model="closeData.newYearEnd"
class="txt_calendar_end"
/>
<CustomDatePicker
custom-input=".txt_calendar_end"
append-to="body"
v-model="closeData.newYearEnd"
:min="closeData.newYearStart"
/>
</div>
</div>
<!-- دکمه بستن سال مالی -->
<div class="row">
<div class="col-12">
<v-btn
type="submit"
@click="closeYear()"
color="primary"
prepend-icon="mdi-content-save"
:loading="isLoading"
:disabled="isLoading"
:title="$t('drawer.close_year')"
>
{{ $t('drawer.close_year') }}
</v-btn>
</div>
</div>
</v-form>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Dialog تایید بستن سال مالی -->
<v-dialog v-model="showConfirmDialog" max-width="500px">
<v-card>
<v-card-title class="text-h5">
<v-icon color="warning" class="me-2">mdi-alert-circle</v-icon>
تایید بستن سال مالی
</v-card-title>
<v-card-text>
<p>آیا از بستن سال مالی اطمینان دارید؟</p>
<p class="text-caption text-grey">این عملیات غیرقابل بازگشت است.</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="showConfirmDialog = false">
انصراف
</v-btn>
<v-btn color="primary" @click="confirmCloseYear" :loading="isLoading">
تایید
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Dialog موفقیت بستن سال مالی -->
<v-dialog v-model="showSuccessDialog" max-width="500px" persistent>
<v-card>
<v-card-title class="text-h5">
<v-icon color="success" class="me-2">mdi-check-circle</v-icon>
سال مالی جدید ایجاد شد
</v-card-title>
<v-card-text>
<p>سال مالی جدید با موفقیت ایجاد شد.</p>
<p class="text-caption text-grey">حالا میتوانید سال مالی جدید را انتخاب کنید.</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" @click="goToBusinessProfile">
تایید
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Snackbar for notifications -->
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="snackbar.timeout"
location="top"
>
{{ snackbar.text }}
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</template>
<script>
import axios from "axios";
import Loading from 'vue-loading-overlay';
import moment from 'moment-jalaali';
export default {
name: "table",
name: "CloseYear",
components: {
Loading
},
data: () => {
return {
isLoading: false,
YearInfo: {
banks: {
bs: 0,
bd: 0
},
cashdesks: {
bs: 0,
bd: 0
},
salarys: {
bs: 0,
bd: 0
},
persons: {
bs: 0,
bd: 0
},
year: {
label: ''
}
showConfirmDialog: false,
showSuccessDialog: false,
snackbar: {
show: false,
text: '',
color: 'success',
timeout: 5000
},
currentYear: {
id: null,
label: '',
start: '',
end: ''
},
profitLoss: {
totalIncome: 0,
totalExpense: 0,
netProfit: 0
},
balanceSheet: {
totalAssets: 0,
totalLiabilities: 0,
totalEquity: 0
},
accountsTree: [], // New data property for accounts tree
closeData: {
taxPercent: 0,
dividendPercent: 0,
newYearLabel: '',
newYearStart: '',
newYearEnd: ''
}
}
},
computed: {
taxAmount() {
return (this.profitLoss.netProfit * this.closeData.taxPercent) / 100;
},
netProfitAfterTax() {
return this.profitLoss.netProfit - this.taxAmount;
},
dividendAmount() {
return (this.netProfitAfterTax * this.closeData.dividendPercent) / 100;
},
retainedEarnings() {
return this.netProfitAfterTax - this.dividendAmount;
}
},
mounted() {
this.loadData();
},
methods: {
loadData() {
axios.post('/api/year/lastyear/info').then((Response) => {
this.YearInfo = Response.data;
})
}
}
showSnackbar(text, color = 'success', timeout = 5000) {
this.snackbar.text = text;
this.snackbar.color = color;
this.snackbar.timeout = timeout;
this.snackbar.show = true;
},
async loadData() {
this.isLoading = true;
try {
const response = await axios.post('/api/year/close/prepare');
if (response.data.result === 1) {
this.currentYear = response.data.currentYear;
this.profitLoss = response.data.profitLoss;
this.balanceSheet = response.data.balanceSheet;
this.accountsTree = response.data.accountsTree; // Assign accounts tree data
// تنظیم مقادیر پیشفرض سال مالی جدید
await this.setupNewYearDefaults();
} else {
this.showSnackbar(response.data.msg || 'خطا در بارگذاری اطلاعات', 'error');
}
} catch (error) {
console.error('Error loading data:', error);
let errorMessage = 'خطا در بارگذاری اطلاعات';
try {
if (error && error.response && error.response.data && error.response.data.msg) {
errorMessage = error.response.data.msg;
} else if (error && error.message) {
errorMessage = error.message;
}
} catch (e) {
console.error('Error parsing error message:', e);
}
this.showSnackbar(errorMessage, 'error');
} finally {
this.isLoading = false;
}
},
async setupNewYearDefaults() {
// محاسبه سال مالی جدید بر اساس سال مالی جاری
if (this.currentYear.end) {
let currentEndDate;
// بررسی اینکه آیا تاریخ timestamp است یا فرمت شمسی
if (typeof this.currentYear.end === 'number' || !isNaN(this.currentYear.end)) {
// اگر timestamp است، به تاریخ شمسی تبدیل کن
const timestamp = parseInt(this.currentYear.end);
const jDate = moment.unix(timestamp);
currentEndDate = [
jDate.jYear().toString(),
jDate.jMonth().toString().padStart(2, '0'),
jDate.jDate().toString().padStart(2, '0')
];
} else {
// اگر فرمت شمسی است، مستقیماً استفاده کن
currentEndDate = this.currentYear.end.split('/');
}
const nextYear = parseInt(currentEndDate[0]) + 1;
// سال مالی جدید باید از روز بعد از پایان سال مالی جاری شروع شود
// برای جلوگیری از تداخل زمانی
const currentEndMoment = moment(`${currentEndDate[0]}/${currentEndDate[1]}/${currentEndDate[2]}`, 'jYYYY/jMM/jDD');
const nextDay = currentEndMoment.add(1, 'day');
this.closeData.newYearStart = nextDay.format('jYYYY/jMM/jDD');
this.closeData.newYearEnd = `${nextYear}/${currentEndDate[1]}/${currentEndDate[2]}`;
this.closeData.newYearLabel = `سال مالی منتهی به ${this.closeData.newYearEnd}`;
}
},
async closeYear() {
const { valid } = await this.$refs.form.validate();
if (valid) {
this.showConfirmDialog = true;
}
},
async confirmCloseYear() {
this.showConfirmDialog = false;
// اعتبارسنجی تاریخها
if (!(await this.validateDates())) {
return;
}
this.isLoading = true;
try {
const response = await axios.post('/api/year/close/execute', this.closeData);
if (response.data.result === 1) {
this.showSnackbar(response.data.msg, 'success');
this.showSuccessDialog = true;
} else {
this.showSnackbar(response.data.msg || 'خطا در بستن سال مالی', 'error');
}
} catch (error) {
console.error('Error closing year:', error);
let errorMessage = 'خطا در بستن سال مالی';
try {
if (error && error.response && error.response.data && error.response.data.msg) {
errorMessage = error.response.data.msg;
} else if (error && error.message) {
errorMessage = error.message;
}
} catch (e) {
console.error('Error parsing error message:', e);
}
this.showSnackbar(errorMessage, 'error');
} finally {
this.isLoading = false;
}
},
async validateDates() {
try {
// بررسی وجود تاریخها
if (!this.closeData.newYearStart || !this.closeData.newYearEnd) {
this.showSnackbar('تاریخ شروع و پایان سال مالی الزامی است', 'error');
return false;
}
try {
// اعتبارسنجی از طریق API
const response = await axios.post('/api/year/close/validate', this.closeData);
if (response.data.result === 0) {
this.showSnackbar(response.data.msg, 'error');
return false;
}
return true;
} catch (error) {
console.error('Validation error:', error);
// سادهسازی error handling
let errorMessage = 'خطا در اعتبارسنجی تاریخ‌ها';
try {
// Check for axios error response
if (error && error.response && error.response.data && error.response.data.msg) {
errorMessage = error.response.data.msg;
}
// Check for network error
else if (error && error.message) {
errorMessage = error.message;
}
// Check for any other error type
else if (error && typeof error === 'string') {
errorMessage = error;
}
} catch (parseError) {
console.error('Error parsing error message:', parseError);
errorMessage = 'خطا در اعتبارسنجی تاریخ‌ها';
}
this.showSnackbar(errorMessage, 'error');
return false;
}
} catch (outerError) {
console.error('Unexpected error in validateDates:', outerError);
this.showSnackbar('خطای غیرمنتظره در اعتبارسنجی تاریخ‌ها', 'error');
return false;
}
},
convertPersianDateToTimestamp(persianDate) {
// اگر تاریخ به صورت timestamp است، مستقیماً برگردان
if (!isNaN(persianDate)) {
return parseInt(persianDate);
}
// تبدیل تاریخ شمسی به میلادی و سپس به timestamp
const parts = persianDate.split('/');
if (parts.length !== 3) {
throw new Error('فرمت تاریخ نامعتبر است');
}
const year = parseInt(parts[0]);
const month = parseInt(parts[1]);
const day = parseInt(parts[2]);
// تبدیل سال شمسی به میلادی (تقریبی)
const gregorianYear = year + 621;
// ایجاد تاریخ میلادی
const date = new Date(gregorianYear, month - 1, day);
date.setHours(0, 0, 0, 0);
return Math.floor(date.getTime() / 1000);
},
getAccountTypeClass(type) {
switch (type) {
case 'asset':
return 'bg-primary text-light';
case 'liability':
return 'bg-info text-light';
case 'equity':
return 'bg-success text-light';
case 'income':
return 'bg-warning text-dark';
case 'expense':
return 'bg-danger text-light';
default:
return '';
}
},
goToBusinessProfile() {
this.showSuccessDialog = false;
this.$router.push('/profile/business');
}
},
watch: {
// Removed the problematic watcher that was overriding correct date calculations
}
}
</script>
<style scoped></style>
<style scoped>
.alert {
margin-bottom: 20px;
}
.block {
margin-bottom: 20px;
}
.form-label {
font-weight: bold;
}
.btn-lg {
padding: 12px 30px;
font-size: 16px;
}
.account-tree {
max-height: 600px;
overflow-y: auto;
}
.account-item {
margin-bottom: 10px;
}
.account-header {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 5px;
}
.account-code {
font-weight: bold;
margin-right: 10px;
min-width: 60px;
}
.account-name {
flex: 1;
margin-right: 10px;
}
.account-balance {
font-weight: bold;
margin-right: 10px;
min-width: 120px;
text-align: left;
}
.account-type-badge {
font-size: 0.75rem;
padding: 2px 6px;
}
.account-children {
margin-left: 20px;
border-left: 2px solid #e9ecef;
padding-left: 15px;
}
.account-children .account-item {
margin-bottom: 5px;
}
.account-children .account-children {
margin-left: 15px;
}
.child .account-header {
font-size: 0.9rem;
padding: 6px 10px;
}
.grandchild .account-header {
font-size: 0.85rem;
padding: 4px 8px;
}
/* تقویم شمسی */
.txt_calendar_start,
.txt_calendar_end {
cursor: pointer;
background-color: #fff;
}
.txt_calendar_start:focus,
.txt_calendar_end:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
</style>

View file

@ -131,7 +131,7 @@ export default {
rowsPerPageMessage="تعداد سطر" emptyMessage="اطلاعاتی برای نمایش وجود ندارد"
rowsOfPageSeparatorMessage="از" theme-color="#1d90ff" header-text-direction="center"
body-text-direction="center" :loading="loading">
<template #item-operation="props: any">
<template #item-operation="props">
<v-btn variant="text" @click="selected = props; dialog = true;" color="success" icon="mdi-file-edit"
v-bind="props"></v-btn>
</template>