From fa46e410fc5b46e382889430c07ad3d17cdc04af Mon Sep 17 00:00:00 2001 From: Babak Alizadeh Date: Thu, 21 Aug 2025 21:09:26 +0000 Subject: [PATCH] add sell report --- .../src/Controller/SellReportController.php | 519 ++++++++++ hesabixCore/src/Service/SellReportService.php | 903 +++++++++++++++++ webUI/src/router/index.ts | 10 + .../views/acc/reports/explore_accounts.vue | 5 +- webUI/src/views/acc/reports/reports.vue | 21 + .../src/views/acc/reports/sell/SellReport.vue | 906 ++++++++++++++++++ .../acc/reports/sell/components/SellChart.vue | 147 +++ .../reports/sell/components/SellSummary.vue | 68 ++ 8 files changed, 2578 insertions(+), 1 deletion(-) create mode 100644 hesabixCore/src/Controller/SellReportController.php create mode 100644 hesabixCore/src/Service/SellReportService.php create mode 100644 webUI/src/views/acc/reports/sell/SellReport.vue create mode 100644 webUI/src/views/acc/reports/sell/components/SellChart.vue create mode 100644 webUI/src/views/acc/reports/sell/components/SellSummary.vue diff --git a/hesabixCore/src/Controller/SellReportController.php b/hesabixCore/src/Controller/SellReportController.php new file mode 100644 index 0000000..ae57fe5 --- /dev/null +++ b/hesabixCore/src/Controller/SellReportController.php @@ -0,0 +1,519 @@ +hasRole('sell'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + // بررسی فعال بودن پلاگین accpro + if (!$pluginService->isActive('accpro', $acc['bid'])) { + return $this->json([ + 'result' => 0, + 'message' => 'پلاگین accpro فعال نیست' + ], 403); + } + + // دریافت پارامترها + $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'); + + // تنظیم تاریخ‌های پیش‌فرض اگر ارسال نشده باشند + if (!$startDate) { + $startDate = $jdate->jdate('Y/m/01', time()); // ابتدای ماه جاری + } + if (!$endDate) { + $endDate = $jdate->jdate('Y/m/d', time()); // امروز + } + + try { + $summary = $sellReportService->getSellSummary( + $acc['bid'], + $acc['year'], + $acc['money'], + $startDate, + $endDate, + $groupBy, + $customerId, + $status + ); + + return $this->json([ + 'result' => 1, + 'data' => $summary + ]); + + } catch (\Exception $e) { + return $this->json([ + 'result' => 0, + 'message' => $e->getMessage() + ], 500); + } + } + + #[Route('/api/sell/report/invoices', name: 'app_sell_report_invoices', methods: ['GET'])] + public function getSellInvoices( + Request $request, + Access $access, + EntityManagerInterface $entityManager, + SellReportService $sellReportService, + PluginService $pluginService + ): JsonResponse { + // بررسی دسترسی + $acc = $access->hasRole('sell'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + // بررسی فعال بودن پلاگین accpro + if (!$pluginService->isActive('accpro', $acc['bid'])) { + return $this->json([ + 'result' => 0, + 'message' => 'پلاگین accpro فعال نیست' + ], 403); + } + + // دریافت پارامترها + $startDate = $request->query->get('startDate'); + $endDate = $request->query->get('endDate'); + $customerId = $request->query->get('customerId'); + $commodityId = $request->query->get('commodityId'); + $status = $request->query->get('status'); + $page = max(1, (int) $request->query->get('page', 1)); + $perPage = max(1, min(100, (int) $request->query->get('perPage', 20))); + + try { + $invoices = $sellReportService->getSellInvoices( + $acc['bid'], + $acc['year'], + $acc['money'], + $startDate, + $endDate, + $customerId, + $commodityId, + $status, + $page, + $perPage + ); + + return $this->json([ + 'result' => 1, + 'data' => $invoices + ]); + + } catch (\Exception $e) { + return $this->json([ + 'result' => 0, + 'message' => $e->getMessage() + ], 500); + } + } + + #[Route('/api/sell/report/top-products', name: 'app_sell_report_top_products', methods: ['GET'])] + public function getTopProducts( + Request $request, + Access $access, + EntityManagerInterface $entityManager, + SellReportService $sellReportService, + PluginService $pluginService + ): JsonResponse { + // بررسی دسترسی + $acc = $access->hasRole('sell'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + // بررسی فعال بودن پلاگین accpro + if (!$pluginService->isActive('accpro', $acc['bid'])) { + return $this->json([ + 'result' => 0, + 'message' => 'پلاگین accpro فعال نیست' + ], 403); + } + + // دریافت پارامترها + $startDate = $request->query->get('startDate'); + $endDate = $request->query->get('endDate'); + $limit = max(1, min(50, (int) $request->query->get('limit', 10))); + $sortBy = $request->query->get('sortBy', 'amount'); + $customerId = $request->query->get('customerId'); + $status = $request->query->get('status'); + + try { + $topProducts = $sellReportService->getTopProducts( + $acc['bid'], + $acc['year'], + $acc['money'], + $startDate, + $endDate, + $limit, + $sortBy, + $customerId, + $status + ); + + return $this->json([ + 'result' => 1, + 'data' => $topProducts + ]); + + } catch (\Exception $e) { + return $this->json([ + 'result' => 0, + 'message' => $e->getMessage() + ], 500); + } + } + + #[Route('/api/sell/report/top-customers', name: 'app_sell_report_top_customers', methods: ['GET'])] + public function getTopCustomers( + Request $request, + Access $access, + EntityManagerInterface $entityManager, + SellReportService $sellReportService, + PluginService $pluginService + ): JsonResponse { + // بررسی دسترسی + $acc = $access->hasRole('sell'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + // بررسی فعال بودن پلاگین accpro + if (!$pluginService->isActive('accpro', $acc['bid'])) { + return $this->json([ + 'result' => 0, + 'message' => 'پلاگین accpro فعال نیست' + ], 403); + } + + // دریافت پارامترها + $startDate = $request->query->get('startDate'); + $endDate = $request->query->get('endDate'); + $limit = max(1, min(50, (int) $request->query->get('limit', 10))); + $customerId = $request->query->get('customerId'); + $status = $request->query->get('status'); + + try { + $topCustomers = $sellReportService->getTopCustomers( + $acc['bid'], + $acc['year'], + $acc['money'], + $startDate, + $endDate, + $limit, + $customerId, + $status + ); + + return $this->json([ + 'result' => 1, + 'data' => $topCustomers + ]); + + } catch (\Exception $e) { + return $this->json([ + 'result' => 0, + 'message' => $e->getMessage() + ], 500); + } + } + + #[Route('/api/sell/report/chart', name: 'app_sell_report_chart', methods: ['GET'])] + public function getSellChart( + Request $request, + Access $access, + EntityManagerInterface $entityManager, + SellReportService $sellReportService, + PluginService $pluginService + ): JsonResponse { + // بررسی دسترسی + $acc = $access->hasRole('sell'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + // بررسی فعال بودن پلاگین accpro + if (!$pluginService->isActive('accpro', $acc['bid'])) { + return $this->json([ + 'result' => 0, + 'message' => 'پلاگین accpro فعال نیست' + ], 403); + } + + // دریافت پارامترها + $startDate = $request->query->get('startDate'); + $endDate = $request->query->get('endDate'); + $groupBy = $request->query->get('groupBy', 'day'); + $type = $request->query->get('type', 'amount'); + + try { + $chartData = $sellReportService->getSellChart( + $acc['bid'], + $acc['year'], + $acc['money'], + $startDate, + $endDate, + $groupBy, + $type + ); + + return $this->json([ + 'result' => 1, + 'data' => $chartData + ]); + + } catch (\Exception $e) { + return $this->json([ + 'result' => 0, + 'message' => $e->getMessage() + ], 500); + } + } + + #[Route('/api/sell/report/customer-analysis', name: 'app_sell_report_customer_analysis', methods: ['GET'])] + public function getCustomerAnalysis( + Request $request, + Access $access, + EntityManagerInterface $entityManager, + SellReportService $sellReportService, + PluginService $pluginService + ): JsonResponse { + // بررسی دسترسی + $acc = $access->hasRole('sell'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + // بررسی فعال بودن پلاگین accpro + if (!$pluginService->isActive('accpro', $acc['bid'])) { + return $this->json([ + 'result' => 0, + 'message' => 'پلاگین accpro فعال نیست' + ], 403); + } + + // دریافت پارامترها + $startDate = $request->query->get('startDate'); + $endDate = $request->query->get('endDate'); + + try { + $customerAnalysis = $sellReportService->getCustomerAnalysis( + $acc['bid'], + $acc['year'], + $acc['money'], + $startDate, + $endDate + ); + + return $this->json([ + 'result' => 1, + 'data' => $customerAnalysis + ]); + + } catch (\Exception $e) { + return $this->json([ + 'result' => 0, + 'message' => $e->getMessage() + ], 500); + } + } + + #[Route('/api/sell/report/export', name: 'app_sell_report_export', methods: ['POST'])] + public function exportReport( + Request $request, + Access $access, + EntityManagerInterface $entityManager, + SellReportService $sellReportService, + PluginService $pluginService, + Provider $provider, + Jdate $jdate + ): BinaryFileResponse|JsonResponse { + $acc = $access->hasRole('sell'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + // بررسی فعال بودن پلاگین accpro + if (!$pluginService->isActive('accpro', $acc['bid'])) { + return $this->json([ + 'result' => 0, + 'message' => 'پلاگین accpro فعال نیست' + ], 403); + } + + $params = json_decode($request->getContent(), true) ?? []; + $startDate = $params['startDate'] ?? null; + $endDate = $params['endDate'] ?? null; + $customerId = $params['customerId'] ?? null; + $status = $params['status'] ?? null; + + try { + // دریافت تمام داده‌های گزارش + $summary = $sellReportService->getSellSummary( + $acc['bid'], + $acc['year'], + $acc['money'], + $startDate, + $endDate, + 'day', + $customerId, + $status + ); + + $topProducts = $sellReportService->getTopProducts( + $acc['bid'], + $acc['year'], + $acc['money'], + $startDate, + $endDate, + 100, // تعداد بیشتر برای export + 'amount', + $customerId, + $status + ); + + $topCustomers = $sellReportService->getTopCustomers( + $acc['bid'], + $acc['year'], + $acc['money'], + $startDate, + $endDate, + 100, // تعداد بیشتر برای export + $customerId, + $status + ); + + $invoices = $sellReportService->getSellInvoices( + $acc['bid'], + $acc['year'], + $acc['money'], + $startDate, + $endDate, + $customerId, + null, + $status, + 1, + 1000 // تعداد بیشتر برای export + ); + + // آماده‌سازی داده‌ها برای Excel + $excelData = []; + + // 1. خلاصه آمار + $excelData[] = ['خلاصه آمار فروش']; + $excelData[] = ['', '']; // خط خالی + $excelData[] = ['کل فروش', number_format($summary['totalAmount']) . ' ریال']; + $excelData[] = ['تعداد فاکتور', number_format($summary['totalCount'])]; + $excelData[] = ['میانگین فاکتور', number_format($summary['averageAmount']) . ' ریال']; + $excelData[] = ['کل سود', number_format($summary['totalProfit']) . ' ریال']; + $excelData[] = ['درصد سود', $summary['profitMargin'] . '%']; + $excelData[] = ['بیشترین مبلغ', number_format($summary['maxAmount']) . ' ریال']; + $excelData[] = ['کمترین مبلغ', number_format($summary['minAmount']) . ' ریال']; + $excelData[] = ['', '']; // خط خالی + + // 2. محصولات برتر + $excelData[] = ['محصولات پرفروش']; + $excelData[] = ['', '']; // خط خالی + $excelData[] = ['نام کالا', 'کد', 'تعداد', 'مبلغ کل', 'سود', 'درصد سود']; + + foreach ($topProducts as $product) { + $excelData[] = [ + $product['name'], + $product['code'], + number_format($product['totalCount']), + number_format($product['totalAmount']) . ' ریال', + number_format($product['profit']) . ' ریال', + $product['profitMargin'] . '%' + ]; + } + $excelData[] = ['', '']; // خط خالی + + // 3. مشتریان برتر + $excelData[] = ['مشتریان پرفروش']; + $excelData[] = ['', '']; // خط خالی + $excelData[] = ['نام مشتری', 'کد', 'تعداد فاکتور', 'مبلغ کل', 'میانگین فاکتور']; + + foreach ($topCustomers as $customer) { + $excelData[] = [ + $customer['name'], + $customer['code'], + number_format($customer['invoiceCount']), + number_format($customer['totalAmount']) . ' ریال', + number_format($customer['averageAmount']) . ' ریال' + ]; + } + $excelData[] = ['', '']; // خط خالی + + // 4. لیست فاکتورها + $excelData[] = ['لیست فاکتورها']; + $excelData[] = ['', '']; // خط خالی + $excelData[] = ['شماره فاکتور', 'تاریخ', 'مشتری', 'مبلغ', 'سود', 'درصد سود', 'وضعیت']; + + foreach ($invoices['invoices'] as $invoice) { + $excelData[] = [ + $invoice['code'], + $invoice['date'], + $invoice['customer']['name'] ?? '', + number_format($invoice['amount']) . ' ریال', + number_format($invoice['profit']) . ' ریال', + $invoice['profitMargin'] . '%', + $invoice['isApproved'] ? 'تایید شده' : 'پیش‌نمایش' + ]; + } + + // هدرهای Excel + $headers = [ + 'گزارش فروش - ' . $acc['bid']->getName(), + 'تاریخ شروع: ' . ($startDate ?: 'همه'), + 'تاریخ پایان: ' . ($endDate ?: 'همه'), + 'تاریخ ایجاد: ' . $jdate->jdate('Y/m/d H:i:s', time()) + ]; + + // ایجاد فایل Excel + $filePath = $provider->createExcellFromArray($excelData, $headers); + + return new BinaryFileResponse($filePath); + + } catch (\Exception $e) { + return $this->json([ + 'result' => 0, + 'message' => 'خطا در ایجاد گزارش: ' . $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/SellReportService.php b/hesabixCore/src/Service/SellReportService.php new file mode 100644 index 0000000..41967c8 --- /dev/null +++ b/hesabixCore/src/Service/SellReportService.php @@ -0,0 +1,903 @@ +entityManager = $entityManager; + $this->connection = $entityManager->getConnection(); + } + + /** + * دریافت خلاصه آمار فروش + */ + public function getSellSummary( + Business $business, + $year, + $money, + ?string $startDate = null, + ?string $endDate = null, + string $groupBy = 'day', + ?int $customerId = null, + ?string $status = null + ): array { + $queryBuilder = $this->entityManager->createQueryBuilder() + ->select('d') + ->from(HesabdariDoc::class, 'd') + ->where('d.bid = :bid') + ->andWhere('d.year = :year') + ->andWhere('d.money = :money') + ->andWhere('d.type = :type') + ->setParameter('bid', $business) + ->setParameter('year', $year) + ->setParameter('money', $money) + ->setParameter('type', 'sell'); + + if ($startDate) { + $queryBuilder->andWhere('d.date >= :startDate') + ->setParameter('startDate', $startDate); + } + if ($endDate) { + $queryBuilder->andWhere('d.date <= :endDate') + ->setParameter('endDate', $endDate); + } + + // فیلتر بر اساس وضعیت + if ($status) { + if ($status === 'approved') { + $queryBuilder->andWhere('d.isApproved = :isApproved') + ->setParameter('isApproved', true); + } elseif ($status === 'preview') { + $queryBuilder->andWhere('d.isPreview = :isPreview') + ->setParameter('isPreview', true); + } + } + + $docs = $queryBuilder->getQuery()->getResult(); + + $totalAmount = 0; + $totalCount = 0; + $maxAmount = 0; + $minAmount = PHP_FLOAT_MAX; + $maxDoc = null; + $minDoc = null; + $totalProfit = 0; + + foreach ($docs as $doc) { + // فیلتر بر اساس مشتری + if ($customerId) { + $hasCustomer = false; + foreach ($doc->getHesabdariRows() as $row) { + if ($row->getPerson() && $row->getPerson()->getId() == $customerId) { + $hasCustomer = true; + break; + } + } + if (!$hasCustomer) { + continue; + } + } + + $amount = (float) $doc->getAmount(); + $totalAmount += $amount; + $totalCount++; + + if ($amount > $maxAmount) { + $maxAmount = $amount; + $maxDoc = $doc; + } + if ($amount < $minAmount && $amount > 0) { + $minAmount = $amount; + $minDoc = $doc; + } + + // محاسبه سود + $totalProfit += $this->calculateDocProfit($doc, $business); + } + + $avgAmount = $totalCount > 0 ? $totalAmount / $totalCount : 0; + + // محاسبه آمار گروه‌بندی شده + $groupedStats = $this->getGroupedStats($docs, $groupBy); + + return [ + 'totalAmount' => round($totalAmount), + 'totalCount' => $totalCount, + 'averageAmount' => round($avgAmount), + 'maxAmount' => round($maxAmount), + 'minAmount' => $minAmount === PHP_FLOAT_MAX ? 0 : round($minAmount), + 'maxDoc' => $maxDoc ? [ + 'code' => $maxDoc->getCode(), + 'date' => $maxDoc->getDate(), + 'amount' => round($maxDoc->getAmount()) + ] : null, + 'minDoc' => $minDoc ? [ + 'code' => $minDoc->getCode(), + 'date' => $minDoc->getDate(), + 'amount' => round($minDoc->getAmount()) + ] : null, + 'totalProfit' => round($totalProfit), + 'profitMargin' => $totalAmount > 0 ? round(($totalProfit / $totalAmount) * 100) : 0, + 'groupedStats' => $groupedStats + ]; + } + + /** + * دریافت لیست فاکتورهای فروش + */ + public function getSellInvoices( + Business $business, + $year, + $money, + ?string $startDate = null, + ?string $endDate = null, + ?int $customerId = null, + ?int $commodityId = null, + ?string $status = null, + int $page = 1, + int $perPage = 20 + ): array { + $queryBuilder = $this->entityManager->createQueryBuilder() + ->select('d') + ->from(HesabdariDoc::class, 'd') + ->leftJoin('d.submitter', 'u') + ->where('d.bid = :bid') + ->andWhere('d.year = :year') + ->andWhere('d.money = :money') + ->andWhere('d.type = :type') + ->setParameter('bid', $business) + ->setParameter('year', $year) + ->setParameter('money', $money) + ->setParameter('type', 'sell'); + + if ($startDate) { + $queryBuilder->andWhere('d.date >= :startDate') + ->setParameter('startDate', $startDate); + } + if ($endDate) { + $queryBuilder->andWhere('d.date <= :endDate') + ->setParameter('endDate', $endDate); + } + // فیلتر مشتری بعداً در PHP اعمال می‌شود + if ($status) { + if ($status === 'approved') { + $queryBuilder->andWhere('d.isApproved = :isApproved') + ->setParameter('isApproved', true); + } elseif ($status === 'preview') { + $queryBuilder->andWhere('d.isPreview = :isPreview') + ->setParameter('isPreview', true); + } + } + + // شمارش کل رکوردها (بدون فیلتر مشتری) + $countQuery = clone $queryBuilder; + $totalCount = $countQuery->select('COUNT(DISTINCT d.id)')->getQuery()->getSingleScalarResult(); + + // دریافت رکوردها با pagination + $queryBuilder->setFirstResult(($page - 1) * $perPage) + ->setMaxResults($perPage) + ->orderBy('d.id', 'DESC'); + + $docs = $queryBuilder->getQuery()->getResult(); + + $invoices = []; + foreach ($docs as $doc) { + $customer = null; + $profit = 0; + + // پیدا کردن مشتری + foreach ($doc->getHesabdariRows() as $row) { + if ($row->getPerson()) { + $customer = $row->getPerson(); + break; + } + } + + // فیلتر بر اساس مشتری + if ($customerId && (!$customer || $customer->getId() != $customerId)) { + continue; + } + + // محاسبه سود + $profit = $this->calculateDocProfit($doc, $business); + + // فیلتر بر اساس کالا + if ($commodityId) { + $hasCommodity = false; + foreach ($doc->getHesabdariRows() as $row) { + if ($row->getCommodity() && $row->getCommodity()->getId() == $commodityId) { + $hasCommodity = true; + break; + } + } + if (!$hasCommodity) { + continue; + } + } + + $invoices[] = [ + 'id' => $doc->getId(), + 'code' => $doc->getCode(), + 'date' => $doc->getDate(), + 'amount' => round($doc->getAmount()), + 'description' => $doc->getDes(), + 'customer' => $customer ? [ + 'id' => $customer->getId(), + 'name' => $customer->getNikename(), + 'code' => $customer->getCode() + ] : null, + 'submitter' => $doc->getSubmitter() ? [ + 'id' => $doc->getSubmitter()->getId(), + 'name' => $doc->getSubmitter()->getFullName() + ] : null, + 'isApproved' => $doc->isApproved(), + 'isPreview' => $doc->isPreview(), + 'profit' => round($profit), + 'profitMargin' => $doc->getAmount() > 0 ? round(($profit / $doc->getAmount()) * 100) : 0 + ]; + } + + return [ + 'invoices' => $invoices, + 'total' => $totalCount, + 'page' => $page, + 'perPage' => $perPage, + 'totalPages' => ceil($totalCount / $perPage) + ]; + } + + /** + * دریافت کالاهای پرفروش + */ + public function getTopProducts( + Business $business, + $year, + $money, + ?string $startDate = null, + ?string $endDate = null, + int $limit = 10, + string $sortBy = 'amount', + ?int $customerId = null, + ?string $status = null + ): array { + // استفاده از Query Builder به جای SQL خام + $queryBuilder = $this->entityManager->createQueryBuilder() + ->select('c.id, c.name, c.code') + ->addSelect('SUM(hr.bs) as total_amount') + ->addSelect('SUM(hr.commdityCount) as total_count') + ->addSelect('COUNT(DISTINCT hd.id) as invoice_count') + ->from(HesabdariDoc::class, 'hd') + ->join('hd.hesabdariRows', 'hr') + ->join('hr.commodity', 'c') + ->where('hd.bid = :bid') + ->andWhere('hd.year = :year') + ->andWhere('hd.money = :money') + ->andWhere('hd.type = :type') + ->andWhere('hr.commodity IS NOT NULL') + ->setParameter('bid', $business) + ->setParameter('year', $year) + ->setParameter('money', $money) + ->setParameter('type', 'sell'); + + if ($startDate) { + $queryBuilder->andWhere('hd.date >= :startDate') + ->setParameter('startDate', $startDate); + } + if ($endDate) { + $queryBuilder->andWhere('hd.date <= :endDate') + ->setParameter('endDate', $endDate); + } + + // فیلتر بر اساس مشتری + if ($customerId) { + $queryBuilder->andWhere('hr.person = :customerId') + ->setParameter('customerId', $customerId); + } + + // فیلتر بر اساس وضعیت + if ($status) { + if ($status === 'approved') { + $queryBuilder->andWhere('hd.isApproved = :isApproved') + ->setParameter('isApproved', true); + } elseif ($status === 'preview') { + $queryBuilder->andWhere('hd.isPreview = :isPreview') + ->setParameter('isPreview', true); + } + } + + $queryBuilder->groupBy('c.id, c.name, c.code'); + + // مرتب‌سازی + switch ($sortBy) { + case 'count': + $queryBuilder->orderBy('total_count', 'DESC'); + break; + case 'amount': + default: + $queryBuilder->orderBy('total_amount', 'DESC'); + break; + } + + $queryBuilder->setMaxResults($limit); + + $results = $queryBuilder->getQuery()->getArrayResult(); + + $products = []; + foreach ($results as $result) { + // محاسبه سود برای هر کالا + $profit = $this->calculateProductProfit($result['id'], $business, $startDate, $endDate); + + $products[] = [ + 'id' => $result['id'], + 'name' => $result['name'], + 'code' => $result['code'], + 'totalAmount' => round((float) $result['total_amount']), + 'totalCount' => round((float) $result['total_count']), + 'invoiceCount' => (int) $result['invoice_count'], + 'profit' => round($profit), + 'profitMargin' => $result['total_amount'] > 0 ? round(($profit / $result['total_amount']) * 100) : 0 + ]; + } + + return $products; + } + + /** + * دریافت مشتریان پرفروش + */ + public function getTopCustomers( + Business $business, + $year, + $money, + ?string $startDate = null, + ?string $endDate = null, + int $limit = 10, + ?int $customerId = null, + ?string $status = null + ): array { + // استفاده از Query Builder به جای SQL خام + $queryBuilder = $this->entityManager->createQueryBuilder() + ->select('p.id, p.nikename, p.code, p.mobile') + ->addSelect('COUNT(DISTINCT hd.id) as invoice_count') + ->addSelect('SUM(hd.amount) as total_amount') + ->from(HesabdariDoc::class, 'hd') + ->join('hd.hesabdariRows', 'hr') + ->join('hr.person', 'p') + ->where('hd.bid = :bid') + ->andWhere('hd.year = :year') + ->andWhere('hd.money = :money') + ->andWhere('hd.type = :type') + ->andWhere('hr.person IS NOT NULL') + ->setParameter('bid', $business) + ->setParameter('year', $year) + ->setParameter('money', $money) + ->setParameter('type', 'sell'); + + if ($startDate) { + $queryBuilder->andWhere('hd.date >= :startDate') + ->setParameter('startDate', $startDate); + } + if ($endDate) { + $queryBuilder->andWhere('hd.date <= :endDate') + ->setParameter('endDate', $endDate); + } + + // فیلتر بر اساس مشتری (برای getTopCustomers منطقی نیست، اما برای سازگاری) + if ($customerId) { + $queryBuilder->andWhere('p.id = :customerId') + ->setParameter('customerId', $customerId); + } + + // فیلتر بر اساس وضعیت + if ($status) { + if ($status === 'approved') { + $queryBuilder->andWhere('hd.isApproved = :isApproved') + ->setParameter('isApproved', true); + } elseif ($status === 'preview') { + $queryBuilder->andWhere('hd.isPreview = :isPreview') + ->setParameter('isPreview', true); + } + } + + $queryBuilder->groupBy('p.id, p.nikename, p.code, p.mobile') + ->orderBy('total_amount', 'DESC') + ->setMaxResults($limit); + + $results = $queryBuilder->getQuery()->getArrayResult(); + + $customers = []; + foreach ($results as $result) { + $customers[] = [ + 'id' => $result['id'], + 'name' => $result['nikename'], + 'code' => $result['code'], + 'mobile' => $result['mobile'], + 'invoiceCount' => (int) $result['invoice_count'], + 'totalAmount' => round((float) $result['total_amount']), + 'averageAmount' => $result['invoice_count'] > 0 ? round($result['total_amount'] / $result['invoice_count']) : 0 + ]; + } + + return $customers; + } + + /** + * دریافت داده‌های نمودار + */ + public function getSellChart( + Business $business, + $year, + $money, + ?string $startDate = null, + ?string $endDate = null, + string $groupBy = 'day', + string $type = 'amount' + ): array { + // استفاده از Query Builder ساده + $queryBuilder = $this->entityManager->createQueryBuilder() + ->select('hd.date') + ->addSelect('COUNT(DISTINCT hd.id) as invoice_count') + ->addSelect('SUM(hd.amount) as total_amount') + ->from(HesabdariDoc::class, 'hd') + ->where('hd.bid = :bid') + ->andWhere('hd.year = :year') + ->andWhere('hd.money = :money') + ->andWhere('hd.type = :type') + ->setParameter('bid', $business) + ->setParameter('year', $year) + ->setParameter('money', $money) + ->setParameter('type', 'sell'); + + if ($startDate) { + $queryBuilder->andWhere('hd.date >= :startDate') + ->setParameter('startDate', $startDate); + } + if ($endDate) { + $queryBuilder->andWhere('hd.date <= :endDate') + ->setParameter('endDate', $endDate); + } + + $queryBuilder->groupBy('hd.date') + ->orderBy('hd.date', 'ASC'); + + $results = $queryBuilder->getQuery()->getArrayResult(); + + $labels = []; + $data = []; + + // گروه‌بندی در PHP + $groupedData = []; + foreach ($results as $result) { + $date = $result['date']; + $groupKey = $this->getGroupKey($date, $groupBy); + + if (!isset($groupedData[$groupKey])) { + $groupedData[$groupKey] = [ + 'invoice_count' => 0, + 'total_amount' => 0, + 'label' => $this->formatGroupLabel($date, $groupBy) + ]; + } + + $groupedData[$groupKey]['invoice_count'] += (int) $result['invoice_count']; + $groupedData[$groupKey]['total_amount'] += (float) $result['total_amount']; + } + + // مرتب‌سازی بر اساس کلید + ksort($groupedData); + + foreach ($groupedData as $group) { + $labels[] = $group['label']; + if ($type === 'count') { + $data[] = $group['invoice_count']; + } else { + $data[] = round($group['total_amount']); + } + } + + return [ + 'labels' => $labels, + 'data' => $data, + 'type' => $type, + 'groupBy' => $groupBy + ]; + } + + /** + * تولید کلید گروه‌بندی + */ + private function getGroupKey(string $date, string $groupBy): string + { + $parts = explode('/', $date); + if (count($parts) !== 3) { + return $date; + } + + $year = $parts[0]; + $month = $parts[1]; + $day = $parts[2]; + + switch ($groupBy) { + case 'week': + // محاسبه هفته (ساده) + $dayOfYear = $this->getDayOfYear($year, $month, $day); + $week = ceil($dayOfYear / 7); + return $year . '-' . str_pad($week, 2, '0', STR_PAD_LEFT); + + case 'month': + return $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT); + + case 'day': + default: + return $date; + } + } + + /** + * فرمت کردن برچسب گروه + */ + private function formatGroupLabel(string $date, string $groupBy): string + { + $parts = explode('/', $date); + if (count($parts) !== 3) { + return $date; + } + + $year = $parts[0]; + $month = $parts[1]; + $day = $parts[2]; + + switch ($groupBy) { + case 'week': + $dayOfYear = $this->getDayOfYear($year, $month, $day); + $week = ceil($dayOfYear / 7); + return 'هفته ' . $week; + + case 'month': + $monthNames = [ + '01' => 'فروردین', + '02' => 'اردیبهشت', + '03' => 'خرداد', + '04' => 'تیر', + '05' => 'مرداد', + '06' => 'شهریور', + '07' => 'مهر', + '08' => 'آبان', + '09' => 'آذر', + '10' => 'دی', + '11' => 'بهمن', + '12' => 'اسفند' + ]; + $monthName = $monthNames[$month] ?? $month; + return $monthName . ' ' . $year; + + case 'day': + default: + return $month . '/' . $day; + } + } + + /** + * محاسبه روز سال + */ + private function getDayOfYear(string $year, string $month, string $day): int + { + $monthDays = [ + '01' => 31, '02' => 31, '03' => 31, '04' => 31, '05' => 31, '06' => 31, + '07' => 30, '08' => 30, '09' => 30, '10' => 30, '11' => 30, '12' => 29 + ]; + + $dayOfYear = (int) $day; + for ($m = 1; $m < (int) $month; $m++) { + $monthKey = str_pad($m, 2, '0', STR_PAD_LEFT); + $dayOfYear += $monthDays[$monthKey]; + } + + return $dayOfYear; + } + + /** + * فرمت کردن برچسب تاریخ + */ + private function formatDateLabel(string $date): string + { + // تبدیل تاریخ شمسی به فرمت مناسب + $parts = explode('/', $date); + if (count($parts) === 3) { + return $parts[1] . '/' . $parts[2]; // ماه/روز + } + return $date; + } + + /** + * فرمت کردن برچسب ماه + */ + private function formatMonthLabel(string $yearMonth): string + { + $parts = explode('-', $yearMonth); + if (count($parts) === 2) { + $year = $parts[0]; + $month = $parts[1]; + + $monthNames = [ + '01' => 'فروردین', + '02' => 'اردیبهشت', + '03' => 'خرداد', + '04' => 'تیر', + '05' => 'مرداد', + '06' => 'شهریور', + '07' => 'مهر', + '08' => 'آبان', + '09' => 'آذر', + '10' => 'دی', + '11' => 'بهمن', + '12' => 'اسفند' + ]; + + $monthName = $monthNames[$month] ?? $month; + return $monthName . ' ' . $year; + } + return $yearMonth; + } + + /** + * تحلیل مشتریان + */ + public function getCustomerAnalysis( + Business $business, + $year, + $money, + ?string $startDate = null, + ?string $endDate = null + ): array { + // مشتریان جدید + $newCustomersSql = " + SELECT COUNT(DISTINCT p.id) as new_customers + FROM person p + JOIN hesabdari_row hr ON hr.person_id = p.id + JOIN hesabdari_doc hd ON hd.id = hr.doc_id + WHERE hd.bid_id = :bid_id + AND hd.year_id = :year_id + AND hd.money_id = :money_id + AND hd.type = 'sell' + AND hd.date >= :start_date + AND hd.date <= :end_date + AND p.id NOT IN ( + SELECT DISTINCT hr2.person_id + FROM hesabdari_row hr2 + JOIN hesabdari_doc hd2 ON hd2.id = hr2.doc_id + WHERE hd2.bid_id = :bid_id + AND hd2.year_id = :year_id + AND hd2.money_id = :money_id + AND hd2.type = 'sell' + AND hd2.date < :start_date + ) + "; + + $params = [ + 'bid_id' => $business->getId(), + 'year_id' => $year->getId(), + 'money_id' => $money->getId(), + 'start_date' => $startDate ?? '1400/01/01', + 'end_date' => $endDate ?? '1500/12/29' + ]; + + $stmt = $this->connection->prepare($newCustomersSql); + $stmt->executeQuery($params); + $newCustomers = $stmt->fetchAssociative(); + + // کل مشتریان + $totalCustomersSql = " + SELECT COUNT(DISTINCT p.id) as total_customers + FROM person p + JOIN hesabdari_row hr ON hr.person_id = p.id + JOIN hesabdari_doc hd ON hd.id = hr.doc_id + WHERE hd.bid_id = :bid_id + AND hd.year_id = :year_id + AND hd.money_id = :money_id + AND hd.type = 'sell' + AND hd.date >= :start_date + AND hd.date <= :end_date + "; + + $stmt = $this->connection->prepare($totalCustomersSql); + $stmt->executeQuery($params); + $totalCustomers = $stmt->fetchAssociative(); + + return [ + 'newCustomers' => (int) $newCustomers['new_customers'], + 'totalCustomers' => (int) $totalCustomers['total_customers'], + 'repeatCustomers' => (int) $totalCustomers['total_customers'] - (int) $newCustomers['new_customers'] + ]; + } + + /** + * محاسبه سود یک فاکتور + */ + private function calculateDocProfit(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') { + // LIFO - آخرین ورودی، اولین خروجی + $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 { + // Average - میانگین قیمت خرید + $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; + } + + /** + * محاسبه سود یک کالا + */ + private function calculateProductProfit(int $commodityId, Business $business, ?string $startDate, ?string $endDate): float + { + $queryBuilder = $this->entityManager->createQueryBuilder() + ->select('hr') + ->from(HesabdariRow::class, 'hr') + ->join('hr.doc', 'hd') + ->where('hr.commodity = :commodityId') + ->andWhere('hd.type = :type') + ->andWhere('hd.bid = :bid') + ->setParameter('commodityId', $commodityId) + ->setParameter('type', 'sell') + ->setParameter('bid', $business); + + if ($startDate) { + $queryBuilder->andWhere('hd.date >= :startDate') + ->setParameter('startDate', $startDate); + } + if ($endDate) { + $queryBuilder->andWhere('hd.date <= :endDate') + ->setParameter('endDate', $endDate); + } + + $rows = $queryBuilder->getQuery()->getResult(); + + $profit = 0; + $profitCalcType = $business->getProfitCalctype() ?? 'simple'; + + foreach ($rows as $row) { + if ($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; + } + + /** + * دریافت آمار گروه‌بندی شده + */ + private function getGroupedStats(array $docs, string $groupBy): array + { + $grouped = []; + + foreach ($docs as $doc) { + $date = $doc->getDate(); + $amount = (float) $doc->getAmount(); + + if ($groupBy === 'day') { + $key = $date; + } elseif ($groupBy === 'month') { + // استخراج ماه از تاریخ شمسی + $parts = explode('/', $date); + $key = $parts[0] . '/' . $parts[1]; + } else { + $key = $date; + } + + if (!isset($grouped[$key])) { + $grouped[$key] = [ + 'date' => $key, + 'amount' => 0, + 'count' => 0 + ]; + } + + $grouped[$key]['amount'] += $amount; + $grouped[$key]['count']++; + } + + // مرتب‌سازی بر اساس تاریخ + ksort($grouped); + + return array_values($grouped); + } +} \ No newline at end of file diff --git a/webUI/src/router/index.ts b/webUI/src/router/index.ts index c5b07aa..095132d 100755 --- a/webUI/src/router/index.ts +++ b/webUI/src/router/index.ts @@ -320,6 +320,16 @@ const router = createRouter({ component: () => import('../views/acc/reports/persons/withdet.vue'), }, + { + path: 'reports/sell', + name: 'sell_report', + component: () => + import('../views/acc/reports/sell/SellReport.vue'), + meta: { + 'title': 'گزارش فروش', + 'login': true + } + }, { path: 'costs/list', name: 'costs_list', diff --git a/webUI/src/views/acc/reports/explore_accounts.vue b/webUI/src/views/acc/reports/explore_accounts.vue index 38c05e2..3bd0cb4 100755 --- a/webUI/src/views/acc/reports/explore_accounts.vue +++ b/webUI/src/views/acc/reports/explore_accounts.vue @@ -358,7 +358,10 @@ this.errorDialog = true; }, formatNumber(value) { - return value ? Number(value).toLocaleString('fa-IR') : '0'; + if (!value) return '0'; + // گرد کردن عدد به نزدیکترین عدد صحیح + const roundedValue = Math.round(Number(value)); + return roundedValue.toLocaleString('fa-IR'); }, formatDateForDisplay(dateString) { if (!dateString) return ''; diff --git a/webUI/src/views/acc/reports/reports.vue b/webUI/src/views/acc/reports/reports.vue index ff27c6e..cfec65b 100755 --- a/webUI/src/views/acc/reports/reports.vue +++ b/webUI/src/views/acc/reports/reports.vue @@ -95,6 +95,24 @@ + + + + + + mdi-cart + فروش + + + + + + + + @@ -122,6 +140,9 @@ export default { accountingReports: [ { text: 'ترازنامه', to: '/acc/reports/acc/balance_sheet' }, { text: this.$t('dialog.explore_accounts'), to: '/acc/reports/acc/explore_accounts' } + ], + salesReports: [ + { text: 'گزارش فروش', to: '/acc/reports/sell' } ] } }, diff --git a/webUI/src/views/acc/reports/sell/SellReport.vue b/webUI/src/views/acc/reports/sell/SellReport.vue new file mode 100644 index 0000000..482f17f --- /dev/null +++ b/webUI/src/views/acc/reports/sell/SellReport.vue @@ -0,0 +1,906 @@ + + + + + + + \ No newline at end of file diff --git a/webUI/src/views/acc/reports/sell/components/SellChart.vue b/webUI/src/views/acc/reports/sell/components/SellChart.vue new file mode 100644 index 0000000..125ed28 --- /dev/null +++ b/webUI/src/views/acc/reports/sell/components/SellChart.vue @@ -0,0 +1,147 @@ + + + + + \ No newline at end of file diff --git a/webUI/src/views/acc/reports/sell/components/SellSummary.vue b/webUI/src/views/acc/reports/sell/components/SellSummary.vue new file mode 100644 index 0000000..4c1eb18 --- /dev/null +++ b/webUI/src/views/acc/reports/sell/components/SellSummary.vue @@ -0,0 +1,68 @@ + + + + + \ No newline at end of file