From 9ad415a23b95efce704860b857870630ec895951 Mon Sep 17 00:00:00 2001 From: Babak Alizadeh Date: Sun, 2 Nov 2025 12:23:19 +0000 Subject: [PATCH] add profit report --- .../src/Controller/ProfitReportController.php | 333 ++++++++++++++++++ hesabixCore/src/Service/SellReportService.php | 57 +++ webUI/package.json | 2 +- webUI/src/router/index.ts | 10 + .../views/acc/reports/profit/ProfitReport.vue | 317 +++++++++++++++++ webUI/src/views/acc/reports/reports.vue | 3 +- 6 files changed, 720 insertions(+), 2 deletions(-) create mode 100644 hesabixCore/src/Controller/ProfitReportController.php create mode 100644 webUI/src/views/acc/reports/profit/ProfitReport.vue diff --git a/hesabixCore/src/Controller/ProfitReportController.php b/hesabixCore/src/Controller/ProfitReportController.php new file mode 100644 index 0000000..8ac63d6 --- /dev/null +++ b/hesabixCore/src/Controller/ProfitReportController.php @@ -0,0 +1,333 @@ +hasRole('sell'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + // بررسی فعال بودن پلاگین accpro + if (!$pluginService->isActive('accpro', $acc['bid'])) { + return $this->json([ + 'result' => 0, + 'message' => 'پلاگین accpro فعال نیست' + ], 403); + } + + // دریافت اطلاعات کسب و کار + $business = $entityManager->getRepository(Business::class)->find($acc['bid']); + if (!$business) { + return $this->json([ + 'result' => 0, + 'message' => 'کسب و کار یافت نشد' + ], 404); + } + + // پارامترها + $startDate = $request->query->get('startDate'); + $endDate = $request->query->get('endDate'); + $groupBy = $request->query->get('groupBy', 'day'); + $customerId = $request->query->get('customerId'); + $status = $request->query->get('status'); + $includeTrend = filter_var($request->query->get('includeTrend', '1'), FILTER_VALIDATE_BOOLEAN); + + // وضعیت فقط در صورت فعال بودن تایید دو مرحله‌ای + if ($status && !$business->isRequireTwoStepApproval()) { + $status = null; + } + + // پیش فرض تاریخ‌ها در صورت عدم ارسال (شمسی) + if (!$startDate) { + $startDate = $jdate->jdate('Y/m/01', time()); + } + if (!$endDate) { + $endDate = $jdate->jdate('Y/m/d', time()); + } + + try { + // خلاصه فروش و سود ناخالص از سرویس موجود + $sellSummary = $sellReportService->getSellSummary( + $acc['bid'], + $acc['year'], + $acc['money'], + $startDate, + $endDate, + $groupBy, + $customerId ? (int) $customerId : null, + $status + ); + + $totalSalesAmount = (float) ($sellSummary['totalAmount'] ?? 0); + $totalGrossProfit = (float) ($sellSummary['totalProfit'] ?? 0); + + // جمع هزینه‌های تایید شده در بازه + $costTotal = (float) ($entityManager->createQueryBuilder() + ->select('COALESCE(SUM(r.bs), 0) as total') + ->from('App\\Entity\\HesabdariDoc', 'd') + ->join('d.hesabdariRows', 'r') + ->where('d.bid = :bid') + ->andWhere('d.money = :money') + ->andWhere('d.type = :type') + ->andWhere('d.year = :year') + ->andWhere('d.isApproved = :isApproved') + ->andWhere('r.bs != 0') + ->andWhere('d.date BETWEEN :start AND :end') + ->setParameter('bid', $acc['bid']) + ->setParameter('money', $acc['money']) + ->setParameter('type', 'cost') + ->setParameter('year', $acc['year']) + ->setParameter('isApproved', true) + ->setParameter('start', $startDate) + ->setParameter('end', $endDate) + ->getQuery() + ->getSingleScalarResult()); + + // جمع درآمدهای غیرعملیاتی تایید شده در بازه + $otherIncomeTotal = (float) ($entityManager->createQueryBuilder() + ->select('COALESCE(SUM(r.bs), 0) as total') + ->from('App\\Entity\\HesabdariDoc', 'd') + ->join('d.hesabdariRows', 'r') + ->where('d.bid = :bid') + ->andWhere('d.money = :money') + ->andWhere('d.type = :type') + ->andWhere('d.year = :year') + ->andWhere('d.isApproved = :isApproved') + ->andWhere('r.bs != 0') + ->andWhere('d.date BETWEEN :start AND :end') + ->setParameter('bid', $acc['bid']) + ->setParameter('money', $acc['money']) + ->setParameter('type', 'income') + ->setParameter('year', $acc['year']) + ->setParameter('isApproved', true) + ->setParameter('start', $startDate) + ->setParameter('end', $endDate) + ->getQuery() + ->getSingleScalarResult()); + + $netProfit = $totalGrossProfit + $otherIncomeTotal - $costTotal; + + $trend = null; + if ($includeTrend) { + // ترند سود ناخالص فروش (با محاسبه سود هر سند) و سود خالص + $trend = $this->buildTrend( + $entityManager, + $sellReportService, + $acc['bid'], + $acc['year'], + $acc['money'], + $startDate, + $endDate, + $groupBy, + $customerId ? (int) $customerId : null, + $status + ); + } + + return $this->json([ + 'result' => 1, + 'data' => [ + 'totalSalesAmount' => (int) round($totalSalesAmount), + 'totalGrossProfit' => (int) round($totalGrossProfit), + 'totalOperatingCosts' => (int) round($costTotal), + 'totalOtherIncome' => (int) round($otherIncomeTotal), + 'netProfit' => (int) round($netProfit), + 'startDate' => $startDate, + 'endDate' => $endDate, + 'trend' => $trend + ] + ]); + } catch (\Exception $e) { + return $this->json([ + 'result' => 0, + 'message' => $e->getMessage() + ], 500); + } + } +} + +// کمکی‌ها: ساخت ترند سود بر اساس گروه‌بندی +namespace App\Controller; + +use App\Entity\HesabdariDoc; +use App\Entity\HesabdariRow; +use Doctrine\ORM\EntityManagerInterface; + +trait ProfitTrendHelper +{ + private function buildTrend( + EntityManagerInterface $entityManager, + $sellReportService, + $business, + $year, + $money, + string $startDate, + string $endDate, + string $groupBy, + ?int $customerId, + ?string $status + ): array { + // فروش های تایید شده در بازه + $qb = $entityManager->createQueryBuilder() + ->select('d') + ->from(HesabdariDoc::class, 'd') + ->where('d.bid = :bid') + ->andWhere('d.year = :year') + ->andWhere('d.money = :money') + ->andWhere('d.type = :type') + ->andWhere('d.date BETWEEN :start AND :end') + ->setParameter('bid', $business) + ->setParameter('year', $year) + ->setParameter('money', $money) + ->setParameter('type', 'sell') + ->setParameter('start', $startDate) + ->setParameter('end', $endDate); + + // در صورت نیاز به تایید دو مرحله‌ای، فقط اسناد تایید شده + $qb->andWhere('d.isApproved = :isApproved')->setParameter('isApproved', true); + + /** @var HesabdariDoc[] $sellDocs */ + $sellDocs = $qb->getQuery()->getResult(); + + $grossByKey = []; + + foreach ($sellDocs as $doc) { + // فیلتر مشتری در سطح ردیف‌ها + if ($customerId) { + $hasCustomer = false; + foreach ($doc->getHesabdariRows() as $row) { + if ($row->getPerson() && $row->getPerson()->getId() === $customerId) { + $hasCustomer = true; + break; + } + } + if (!$hasCustomer) { + continue; + } + } + + $key = $this->groupKeyFromJalali($doc->getDate(), $groupBy); + if (!isset($grossByKey[$key])) { + $grossByKey[$key] = 0.0; + } + $grossByKey[$key] += $sellReportService->computeDocProfit($doc, $business); + } + + // هزینه‌ها بر اساس تاریخ + $costRows = $entityManager->createQueryBuilder() + ->select('d.date as date, SUM(r.bs) as total') + ->from(HesabdariDoc::class, 'd') + ->join('d.hesabdariRows', 'r') + ->where('d.bid = :bid') + ->andWhere('d.year = :year') + ->andWhere('d.money = :money') + ->andWhere('d.type = :type') + ->andWhere('d.isApproved = :isApproved') + ->andWhere('d.date BETWEEN :start AND :end') + ->groupBy('d.date') + ->setParameter('bid', $business) + ->setParameter('year', $year) + ->setParameter('money', $money) + ->setParameter('type', 'cost') + ->setParameter('isApproved', true) + ->setParameter('start', $startDate) + ->setParameter('end', $endDate) + ->getQuery()->getArrayResult(); + + $costByKey = []; + foreach ($costRows as $row) { + $key = $this->groupKeyFromJalali($row['date'], $groupBy); + if (!isset($costByKey[$key])) $costByKey[$key] = 0.0; + $costByKey[$key] += (float) $row['total']; + } + + // درآمدهای غیرعملیاتی + $incomeRows = $entityManager->createQueryBuilder() + ->select('d.date as date, SUM(r.bs) as total') + ->from(HesabdariDoc::class, 'd') + ->join('d.hesabdariRows', 'r') + ->where('d.bid = :bid') + ->andWhere('d.year = :year') + ->andWhere('d.money = :money') + ->andWhere('d.type = :type') + ->andWhere('d.isApproved = :isApproved') + ->andWhere('d.date BETWEEN :start AND :end') + ->groupBy('d.date') + ->setParameter('bid', $business) + ->setParameter('year', $year) + ->setParameter('money', $money) + ->setParameter('type', 'income') + ->setParameter('isApproved', true) + ->setParameter('start', $startDate) + ->setParameter('end', $endDate) + ->getQuery()->getArrayResult(); + + $incomeByKey = []; + foreach ($incomeRows as $row) { + $key = $this->groupKeyFromJalali($row['date'], $groupBy); + if (!isset($incomeByKey[$key])) $incomeByKey[$key] = 0.0; + $incomeByKey[$key] += (float) $row['total']; + } + + // کلیدهای یکتا + $allKeys = array_unique(array_merge(array_keys($grossByKey), array_keys($costByKey), array_keys($incomeByKey))); + sort($allKeys); + + $categories = []; + $grossSeries = []; + $netSeries = []; + + foreach ($allKeys as $key) { + $categories[] = $key; + $gross = $grossByKey[$key] ?? 0.0; + $cost = $costByKey[$key] ?? 0.0; + $inc = $incomeByKey[$key] ?? 0.0; + $grossSeries[] = (int) round($gross); + $netSeries[] = (int) round($gross + $inc - $cost); + } + + return [ + 'categories' => $categories, + 'grossProfit' => $grossSeries, + 'netProfit' => $netSeries, + ]; + } + + private function groupKeyFromJalali(string $date, string $groupBy): string + { + // تاریخ به شکل Y/m/d + if ($groupBy === 'month') { + $parts = explode('/', $date); + return $parts[0] . '/' . $parts[1]; + } + // پیش فرض: روزانه + return $date; + } +} + + + diff --git a/hesabixCore/src/Service/SellReportService.php b/hesabixCore/src/Service/SellReportService.php index 41967c8..4b468e3 100644 --- a/hesabixCore/src/Service/SellReportService.php +++ b/hesabixCore/src/Service/SellReportService.php @@ -783,6 +783,63 @@ class SellReportService return $profit; } + /** + * محاسبه سود یک فاکتور (متد عمومی برای استفاده بیرونی) + */ + public function computeDocProfit(HesabdariDoc $doc, Business $business): float + { + $profit = 0; + $profitCalcType = $business->getProfitCalctype() ?? 'simple'; + + foreach ($doc->getHesabdariRows() as $row) { + if ($row->getCommodity() && $row->getCommdityCount()) { + $commodity = $row->getCommodity(); + $count = $row->getCommdityCount(); + $sellPrice = $row->getBs() / $count; + + if ($profitCalcType === 'simple') { + $buyPrice = $commodity->getPriceBuy() ? (float) $commodity->getPriceBuy() : 0; + $profit += ($sellPrice - $buyPrice) * $count; + } elseif ($profitCalcType === 'lis') { + $lastBuyRow = $this->entityManager->getRepository(HesabdariRow::class) + ->findOneBy([ + 'commodity' => $commodity, + 'bs' => 0 + ], ['id' => 'DESC']); + + if ($lastBuyRow && $lastBuyRow->getCommdityCount() > 0) { + $buyPrice = $lastBuyRow->getBd() / $lastBuyRow->getCommdityCount(); + $profit += ($sellPrice - $buyPrice) * $count; + } else { + $profit += $row->getBs(); + } + } else { + $buyRows = $this->entityManager->getRepository(HesabdariRow::class) + ->findBy([ + 'commodity' => $commodity, + 'bs' => 0 + ], ['id' => 'DESC']); + + $totalBuyAmount = 0; + $totalBuyCount = 0; + foreach ($buyRows as $buyRow) { + $totalBuyAmount += $buyRow->getBd(); + $totalBuyCount += $buyRow->getCommdityCount(); + } + + if ($totalBuyCount > 0) { + $avgBuyPrice = $totalBuyAmount / $totalBuyCount; + $profit += ($sellPrice - $avgBuyPrice) * $count; + } else { + $profit += $row->getBs(); + } + } + } + } + + return $profit; + } + /** * محاسبه سود یک کالا */ diff --git a/webUI/package.json b/webUI/package.json index 98c9656..47af1c1 100755 --- a/webUI/package.json +++ b/webUI/package.json @@ -1,6 +1,6 @@ { "name": "hesabix", - "version": "0.49.8", + "version": "0.60.0", "private": true, "scripts": { "dev": "vite", diff --git a/webUI/src/router/index.ts b/webUI/src/router/index.ts index 84e48be..a9161f9 100755 --- a/webUI/src/router/index.ts +++ b/webUI/src/router/index.ts @@ -330,6 +330,16 @@ const router = createRouter({ 'login': true } }, + { + path: 'reports/profit', + name: 'profit_report', + component: () => + import('../views/acc/reports/profit/ProfitReport.vue'), + meta: { + 'title': 'گزارش سود', + 'login': true + } + }, { path: 'costs/list', name: 'costs_list', diff --git a/webUI/src/views/acc/reports/profit/ProfitReport.vue b/webUI/src/views/acc/reports/profit/ProfitReport.vue new file mode 100644 index 0000000..057d712 --- /dev/null +++ b/webUI/src/views/acc/reports/profit/ProfitReport.vue @@ -0,0 +1,317 @@ + + + + + + + diff --git a/webUI/src/views/acc/reports/reports.vue b/webUI/src/views/acc/reports/reports.vue index 0073b99..5f42448 100755 --- a/webUI/src/views/acc/reports/reports.vue +++ b/webUI/src/views/acc/reports/reports.vue @@ -142,7 +142,8 @@ export default { { text: this.$t('dialog.explore_accounts'), to: '/acc/reports/acc/explore_accounts' } ], salesReports: [ - { text: 'گزارش فروش', to: '/acc/reports/sell' } + { text: 'گزارش فروش', to: '/acc/reports/sell' }, + { text: 'گزارش سود', to: '/acc/reports/profit' } ] } },