add profit report
This commit is contained in:
parent
9074059abd
commit
9ad415a23b
333
hesabixCore/src/Controller/ProfitReportController.php
Normal file
333
hesabixCore/src/Controller/ProfitReportController.php
Normal file
|
|
@ -0,0 +1,333 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Business;
|
||||||
|
use App\Service\Access;
|
||||||
|
use App\Service\Jdate;
|
||||||
|
use App\Service\PluginService;
|
||||||
|
use App\Service\SellReportService;
|
||||||
|
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 ProfitReportController extends AbstractController
|
||||||
|
{
|
||||||
|
use ProfitTrendHelper;
|
||||||
|
#[Route('/api/profit/report/summary', name: 'app_profit_report_summary', methods: ['GET'])]
|
||||||
|
public function getProfitSummary(
|
||||||
|
Request $request,
|
||||||
|
Access $access,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
SellReportService $sellReportService,
|
||||||
|
PluginService $pluginService,
|
||||||
|
Jdate $jdate
|
||||||
|
): 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// دریافت اطلاعات کسب و کار
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -783,6 +783,63 @@ class SellReportService
|
||||||
return $profit;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* محاسبه سود یک کالا
|
* محاسبه سود یک کالا
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hesabix",
|
"name": "hesabix",
|
||||||
"version": "0.49.8",
|
"version": "0.60.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
|
||||||
|
|
@ -330,6 +330,16 @@ const router = createRouter({
|
||||||
'login': true
|
'login': true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'reports/profit',
|
||||||
|
name: 'profit_report',
|
||||||
|
component: () =>
|
||||||
|
import('../views/acc/reports/profit/ProfitReport.vue'),
|
||||||
|
meta: {
|
||||||
|
'title': 'گزارش سود',
|
||||||
|
'login': true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'costs/list',
|
path: 'costs/list',
|
||||||
name: 'costs_list',
|
name: 'costs_list',
|
||||||
|
|
|
||||||
317
webUI/src/views/acc/reports/profit/ProfitReport.vue
Normal file
317
webUI/src/views/acc/reports/profit/ProfitReport.vue
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<v-toolbar color="toolbar" title="گزارش سود" flat>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-tooltip :text="$t('dialog.back')" location="bottom">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" icon="mdi-arrow-right" />
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-container class="pa-4">
|
||||||
|
<v-card variant="outlined" class="mb-4">
|
||||||
|
<v-card-title class="text-subtitle-1 font-weight-bold">
|
||||||
|
گزارش سود کسبوکار
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="3">
|
||||||
|
<v-text-field
|
||||||
|
:model-value="formattedStartDate"
|
||||||
|
label="تاریخ شروع"
|
||||||
|
prepend-inner-icon="mdi-calendar-start"
|
||||||
|
readonly
|
||||||
|
@click="showStartDatePicker = true"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
/>
|
||||||
|
<v-dialog v-model="showStartDatePicker" max-width="400">
|
||||||
|
<v-date-picker
|
||||||
|
v-model="gregorianStartDate"
|
||||||
|
:min="convertJalaliToGregorian(year.start)"
|
||||||
|
:max="convertJalaliToGregorian(year.end)"
|
||||||
|
locale="fa"
|
||||||
|
color="primary"
|
||||||
|
@update:model-value="(value) => { dateFilter.startDate = convertGregorianToJalali(value); gregorianStartDate = value; showStartDatePicker = false; }"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="3">
|
||||||
|
<v-text-field
|
||||||
|
:model-value="formattedEndDate"
|
||||||
|
label="تاریخ پایان"
|
||||||
|
prepend-inner-icon="mdi-calendar-end"
|
||||||
|
readonly
|
||||||
|
@click="showEndDatePicker = true"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
/>
|
||||||
|
<v-dialog v-model="showEndDatePicker" max-width="400">
|
||||||
|
<v-date-picker
|
||||||
|
v-model="gregorianEndDate"
|
||||||
|
:min="convertJalaliToGregorian(dateFilter.startDate || year.start)"
|
||||||
|
:max="convertJalaliToGregorian(year.end)"
|
||||||
|
locale="fa"
|
||||||
|
color="primary"
|
||||||
|
@update:model-value="(value) => { dateFilter.endDate = convertGregorianToJalali(value); gregorianEndDate = value; showEndDatePicker = false; }"
|
||||||
|
/>
|
||||||
|
</v-dialog>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="3">
|
||||||
|
<v-select
|
||||||
|
v-model="groupBy"
|
||||||
|
:items="groupByOptions"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
label="گروهبندی"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="3">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="filters.customerId"
|
||||||
|
label="شناسه مشتری (اختیاری)"
|
||||||
|
prepend-inner-icon="mdi-account"
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="3">
|
||||||
|
<v-select
|
||||||
|
v-model="filters.status"
|
||||||
|
:items="statusOptions"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
label="وضعیت تایید (در صورت فعال بودن)"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="3" class="d-flex align-center">
|
||||||
|
<v-btn color="primary" @click="loadData" :loading="loading" prepend-icon="mdi-refresh">بارگذاری</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="pa-4" variant="outlined">
|
||||||
|
<div class="text-caption">کل فروش</div>
|
||||||
|
<div class="text-h5 mt-1">{{ formatNumber(summary.totalSalesAmount) }}</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="pa-4" variant="outlined">
|
||||||
|
<div class="text-caption">سود ناخالص</div>
|
||||||
|
<div class="text-h5 mt-1">{{ formatNumber(summary.totalGrossProfit) }}</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="pa-4" variant="outlined">
|
||||||
|
<div class="text-caption">هزینهها</div>
|
||||||
|
<div class="text-h5 mt-1">{{ formatNumber(summary.totalOperatingCosts) }}</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="pa-4" variant="outlined">
|
||||||
|
<div class="text-caption">درآمدهای دیگر</div>
|
||||||
|
<div class="text-h5 mt-1">{{ formatNumber(summary.totalOtherIncome) }}</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="pa-4" color="primary" variant="elevated">
|
||||||
|
<div class="text-caption">سود خالص</div>
|
||||||
|
<div class="text-h5 mt-1">{{ formatNumber(summary.netProfit) }}</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-card variant="outlined" class="mt-4">
|
||||||
|
<v-card-title class="text-subtitle-1 font-weight-bold">نمودار ترند سود</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<apexchart
|
||||||
|
type="line"
|
||||||
|
height="300"
|
||||||
|
:options="chartOptions"
|
||||||
|
:series="chartSeries"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import VueApexCharts from 'vue3-apexcharts';
|
||||||
|
import moment from 'jalali-moment';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ProfitReport',
|
||||||
|
components: { apexchart: VueApexCharts },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
// تاریخ
|
||||||
|
dateFilter: {
|
||||||
|
startDate: '',
|
||||||
|
endDate: ''
|
||||||
|
},
|
||||||
|
year: {
|
||||||
|
start: '',
|
||||||
|
end: ''
|
||||||
|
},
|
||||||
|
gregorianStartDate: '',
|
||||||
|
gregorianEndDate: '',
|
||||||
|
showStartDatePicker: false,
|
||||||
|
showEndDatePicker: false,
|
||||||
|
// گروهبندی
|
||||||
|
groupBy: 'day',
|
||||||
|
groupByOptions: [
|
||||||
|
{ text: 'روزانه', value: 'day' },
|
||||||
|
{ text: 'هفتگی', value: 'week' },
|
||||||
|
{ text: 'ماهانه', value: 'month' }
|
||||||
|
],
|
||||||
|
// فیلترها
|
||||||
|
filters: {
|
||||||
|
customerId: null,
|
||||||
|
status: null
|
||||||
|
},
|
||||||
|
statusOptions: [
|
||||||
|
{ text: 'تایید شده', value: 'approved' },
|
||||||
|
{ text: 'پیشنمایش', value: 'preview' }
|
||||||
|
],
|
||||||
|
// خلاصه
|
||||||
|
summary: {
|
||||||
|
totalSalesAmount: 0,
|
||||||
|
totalGrossProfit: 0,
|
||||||
|
totalOperatingCosts: 0,
|
||||||
|
totalOtherIncome: 0,
|
||||||
|
netProfit: 0
|
||||||
|
},
|
||||||
|
// نمودار
|
||||||
|
chartOptions: {
|
||||||
|
chart: { type: 'line', fontFamily: 'Vazir, sans-serif' },
|
||||||
|
xaxis: { categories: [], labels: { style: { fontFamily: 'Vazir, sans-serif' } } },
|
||||||
|
yaxis: { labels: { formatter: (v) => new Intl.NumberFormat('fa-IR').format(v) } },
|
||||||
|
tooltip: { y: { formatter: (v) => new Intl.NumberFormat('fa-IR').format(v) + ' ریال' } },
|
||||||
|
theme: { mode: 'light' }
|
||||||
|
},
|
||||||
|
chartSeries: [
|
||||||
|
{ name: 'سود ناخالص', data: [] },
|
||||||
|
{ name: 'سود خالص', data: [] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
formattedStartDate() {
|
||||||
|
return this.dateFilter.startDate ? this.formatDateForDisplay(this.dateFilter.startDate) : '';
|
||||||
|
},
|
||||||
|
formattedEndDate() {
|
||||||
|
return this.dateFilter.endDate ? this.formatDateForDisplay(this.dateFilter.endDate) : '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'dateFilter.startDate'() { if (this.dateFilter.startDate && this.dateFilter.endDate) { this.syncGregorian(); this.loadData(); }},
|
||||||
|
'dateFilter.endDate'() { if (this.dateFilter.startDate && this.dateFilter.endDate) { this.syncGregorian(); this.loadData(); }}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.loadYearBounds();
|
||||||
|
await this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadYearBounds() {
|
||||||
|
try {
|
||||||
|
// دریافت محدوده سال مالی از API مشابه SellReport
|
||||||
|
const yearResponse = await axios.get('/api/year/get');
|
||||||
|
this.year = yearResponse.data || this.year;
|
||||||
|
// مقداردهی پیشفرض فیلتر تاریخ بر اساس سال مالی
|
||||||
|
this.dateFilter.startDate = this.year.start;
|
||||||
|
this.dateFilter.endDate = this.year.end;
|
||||||
|
this.gregorianStartDate = this.convertJalaliToGregorian(this.dateFilter.startDate);
|
||||||
|
this.gregorianEndDate = this.convertJalaliToGregorian(this.dateFilter.endDate);
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
syncGregorian() {
|
||||||
|
this.gregorianStartDate = this.convertJalaliToGregorian(this.dateFilter.startDate);
|
||||||
|
this.gregorianEndDate = this.convertJalaliToGregorian(this.dateFilter.endDate);
|
||||||
|
},
|
||||||
|
async loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
startDate: this.dateFilter.startDate || undefined,
|
||||||
|
endDate: this.dateFilter.endDate || undefined,
|
||||||
|
groupBy: this.groupBy,
|
||||||
|
includeTrend: true,
|
||||||
|
customerId: this.filters?.customerId || undefined,
|
||||||
|
status: this.filters?.status || undefined
|
||||||
|
};
|
||||||
|
const { data } = await axios.get('/api/profit/report/summary', { params });
|
||||||
|
if (data && data.result === 1) {
|
||||||
|
this.summary = data.data || this.summary;
|
||||||
|
if (data.data && data.data.trend) {
|
||||||
|
this.chartOptions = { ...this.chartOptions, xaxis: { ...this.chartOptions.xaxis, categories: data.data.trend.categories || [] } };
|
||||||
|
this.chartSeries = [
|
||||||
|
{ name: 'سود ناخالص', data: data.data.trend.grossProfit || [] },
|
||||||
|
{ name: 'سود خالص', data: data.data.trend.netProfit || [] }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// کمکیها (همان الگوی SellReport)
|
||||||
|
formatNumber(number) {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(number || 0);
|
||||||
|
},
|
||||||
|
formatDateForDisplay(dateString) {
|
||||||
|
if (!dateString) return '';
|
||||||
|
try {
|
||||||
|
if (typeof dateString === 'string' && dateString.includes('/')) {
|
||||||
|
// ورودی جلالی است: همان را نمایش بده
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
// در غیر اینصورت تلاش به تبدیل به جلالی خوانا
|
||||||
|
const jalali = moment(dateString, 'YYYY-MM-DD').format('jYYYY/jMM/jDD');
|
||||||
|
return jalali;
|
||||||
|
} catch (e) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
convertJalaliToGregorian(jalaliDate) {
|
||||||
|
if (!jalaliDate) return '';
|
||||||
|
try {
|
||||||
|
if (typeof jalaliDate === 'string' && jalaliDate.includes('/')) {
|
||||||
|
const gregorian = moment(jalaliDate, 'jYYYY/jMM/jDD').format('YYYY-MM-DD');
|
||||||
|
return gregorian;
|
||||||
|
}
|
||||||
|
return jalaliDate;
|
||||||
|
} catch (e) {
|
||||||
|
return jalaliDate;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
convertGregorianToJalali(gregorianDate) {
|
||||||
|
try {
|
||||||
|
const jalali = moment(gregorianDate, 'YYYY-MM-DD').format('jYYYY/jMM/jDD');
|
||||||
|
return jalali;
|
||||||
|
} catch (e) {
|
||||||
|
return gregorianDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -142,7 +142,8 @@ export default {
|
||||||
{ text: this.$t('dialog.explore_accounts'), to: '/acc/reports/acc/explore_accounts' }
|
{ text: this.$t('dialog.explore_accounts'), to: '/acc/reports/acc/explore_accounts' }
|
||||||
],
|
],
|
||||||
salesReports: [
|
salesReports: [
|
||||||
{ text: 'گزارش فروش', to: '/acc/reports/sell' }
|
{ text: 'گزارش فروش', to: '/acc/reports/sell' },
|
||||||
|
{ text: 'گزارش سود', to: '/acc/reports/profit' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue