bug fix and add some new options

This commit is contained in:
Hesabix 2025-06-08 01:42:08 +00:00
parent 914c01ed44
commit 194bc613d3
17 changed files with 1721 additions and 866 deletions

View file

@ -14,6 +14,12 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Writer\Exception;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
class CashdeskController extends AbstractController class CashdeskController extends AbstractController
{ {
@ -268,4 +274,132 @@ class CashdeskController extends AbstractController
'total' => count($transactions) 'total' => count($transactions)
]); ]);
} }
/**
* @throws Exception
*/
#[Route('/api/cashdesk/card/list/excel', name: 'app_cashdesk_card_list_excel')]
public function app_cashdesk_card_list_excel(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): BinaryFileResponse|JsonResponse|StreamedResponse
{
$acc = $access->hasRole('cashdesk');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('code', $params))
throw $this->createNotFoundException();
$cashdesk = $entityManager->getRepository(Cashdesk::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$cashdesk)
throw $this->createNotFoundException();
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'cashdesk' => $cashdesk,
'year'=>$acc['year']
]);
} else {
$transactions = [];
foreach ($params['items'] as $param) {
$prs = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'id' => $param['id'],
'bid' => $acc['bid'],
'cashdesk' => $cashdesk,
'year' => $acc['year']
]);
if ($prs) {
$transactions[] = $prs;
}
}
}
$spreadsheet = new Spreadsheet();
$activeWorksheet = $spreadsheet->getActiveSheet();
$arrayEntity = [
[
'شماره تراکنش',
'تاریخ',
'توضیحات',
'شرح سند',
'تفضیل',
'بستانکار',
'بدهکار',
'سال مالی',
]
];
foreach ($transactions as $transaction) {
$arrayEntity[] = [
$transaction->getId(),
$transaction->getDoc()->getDate(),
$transaction->getDes(),
$transaction->getDoc()->getDes(),
$transaction->getRef()->getName(),
$transaction->getBs(),
$transaction->getBd(),
$transaction->getYear()->getlabel()
];
}
$activeWorksheet->fromArray($arrayEntity, null, 'A1');
$activeWorksheet->setRightToLeft(true);
$writer = new Xlsx($spreadsheet);
$fileName = 'کارت حساب صندوق ' . $cashdesk->getName() . '.xlsx';
$filePath = __DIR__ . '/../../var/' . $fileName;
$writer->save($filePath);
$response = new BinaryFileResponse($filePath);
$response->setContentDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
$fileName
);
return $response;
}
#[Route('/api/cashdesk/card/list/print', name: 'app_cashdesk_card_list_print')]
public function app_cashdesk_card_list_print(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('cashdesk');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('code', $params))
throw $this->createNotFoundException();
$cashdesk = $entityManager->getRepository(Cashdesk::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$cashdesk)
throw $this->createNotFoundException();
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'cashdesk' => $cashdesk,
'year'=>$acc['year']
]);
} else {
$transactions = [];
foreach ($params['items'] as $param) {
$prs = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'id' => $param['id'],
'bid' => $acc['bid'],
'cashdesk' => $cashdesk,
'year'=>$acc['year']
]);
if ($prs) {
$transactions[] = $prs;
}
}
}
$pid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$this->renderView('pdf/cashdesk_card.html.twig', [
'page_title' => 'کارت حساب' . ' ' . $cashdesk->getName(),
'bid' => $acc['bid'],
'items' => $transactions,
'cashdesk' => $cashdesk
])
);
return $this->json(['id' => $pid]);
}
} }

View file

@ -33,6 +33,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use App\Repository\HesabdariTableRepository;
class HesabdariController extends AbstractController class HesabdariController extends AbstractController
{ {
@ -179,96 +180,192 @@ class HesabdariController extends AbstractController
]); ]);
} }
#[Route('/api/accounting/search', name: 'app_accounting_search')] #[Route('/api/accounting/search', name: 'app_hesabdari_search', methods: ['POST'])]
public function app_accounting_search(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse public function search(
{ Request $request,
$params = []; Access $access,
if ($content = $request->getContent()) { EntityManagerInterface $entityManager,
$params = json_decode($content, true); HesabdariTableRepository $hesabdariTableRepository,
} Jdate $jdate
if (!array_key_exists('type', $params)) ): JsonResponse {
$this->createNotFoundException(); $acc = $access->hasRole('acc');
$roll = ''; if (!$acc) {
if ($params['type'] == 'person_receive' || $params['type'] == 'person_send')
$roll = 'person';
elseif ($params['type'] == 'cost')
$roll = 'cost';
elseif ($params['type'] == 'income')
$roll = 'income';
elseif ($params['type'] == 'buy')
$roll = 'buy';
elseif ($params['type'] == 'rfbuy')
$roll = 'plugAccproRfbuy';
elseif ($params['type'] == 'transfer')
$roll = 'bankTransfer';
elseif ($params['type'] == 'sell')
$roll = 'sell';
elseif ($params['type'] == 'rfsell')
$roll = 'plugAccproRfsell';
elseif ($params['type'] == 'all')
$roll = 'accounting';
else
$this->createNotFoundException();
$acc = $access->hasRole($roll);
if (!$acc)
throw $this->createAccessDeniedException(); throw $this->createAccessDeniedException();
if ($params['type'] == 'all') { }
$data = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'], $params = json_decode($request->getContent(), true) ?? [];
'year' => $acc['year'],
'money' => $acc['money'] // Input parameters
], [ $filters = $params['filters'] ?? [];
'id' => 'DESC' $pagination = $params['pagination'] ?? ['page' => 1, 'limit' => 10];
]); $sort = $params['sort'] ?? ['sortBy' => 'id', 'sortDesc' => true];
$type = $params['type'] ?? 'all';
// Set pagination parameters
$page = max(1, $pagination['page'] ?? 1);
$limit = max(1, min(100, $pagination['limit'] ?? 10));
// Build base query
$queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('u.fullName as submitter')
->from('App\Entity\HesabdariDoc', 'd')
->leftJoin('d.submitter', 'u')
->leftJoin('d.hesabdariRows', 'r')
->leftJoin('r.ref', 't')
->where('d.bid = :bid')
->andWhere('d.year = :year')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
// Add type filter if not 'all'
if ($type !== 'all') {
$queryBuilder->andWhere('d.type = :type')
->setParameter('type', $type);
}
// Apply filters
if (!empty($filters)) {
// Text search
if (isset($filters['search'])) {
$searchValue = is_array($filters['search']) ? $filters['search']['value'] : $filters['search'];
$queryBuilder->leftJoin('r.person', 'p')
->andWhere(
$queryBuilder->expr()->orX(
'd.code LIKE :search',
'd.des LIKE :search',
'd.date LIKE :search',
'd.amount LIKE :search',
'p.nikename LIKE :search',
't.name LIKE :search',
't.code LIKE :search'
)
)
->setParameter('search', "%{$searchValue}%");
}
// Account filter
if (isset($filters['account'])) {
$accountCodes = $hesabdariTableRepository->findAllSubAccountCodes($filters['account'], $acc['bid']->getId());
if (!empty($accountCodes)) {
$queryBuilder->andWhere('t.code IN (:accountCodes)')
->setParameter('accountCodes', $accountCodes);
} else { } else {
$data = $entityManager->getRepository(HesabdariDoc::class)->findBy([ $queryBuilder->andWhere('1 = 0');
'bid' => $acc['bid'],
'year' => $acc['year'],
'type' => $params['type'],
'money' => $acc['money']
], [
'id' => 'DESC'
]);
} }
}
// Time filter
if (isset($filters['timeFilter'])) {
$today = $jdate->jdate('Y/m/d', time());
switch ($filters['timeFilter']) {
case 'today':
$queryBuilder->andWhere('d.date = :today')
->setParameter('today', $today);
break;
case 'week':
$weekStart = $jdate->jdate('Y/m/d', strtotime('-6 days'));
$queryBuilder->andWhere('d.date BETWEEN :weekStart AND :today')
->setParameter('weekStart', $weekStart)
->setParameter('today', $today);
break;
case 'month':
$monthStart = $jdate->jdate('Y/m/01', time());
$queryBuilder->andWhere('d.date BETWEEN :monthStart AND :today')
->setParameter('monthStart', $monthStart)
->setParameter('today', $today);
break;
case 'custom':
if (isset($filters['date']) && isset($filters['date']['from']) && isset($filters['date']['to'])) {
// تبدیل تاریخ‌های شمسی به میلادی
$fromDate = $filters['date']['from'];
$toDate = $filters['date']['to'];
// اطمینان از فرمت صحیح تاریخ‌ها
if (strpos($fromDate, '/') !== false && strpos($toDate, '/') !== false) {
$queryBuilder->andWhere('d.date BETWEEN :dateFrom AND :dateTo')
->setParameter('dateFrom', $fromDate)
->setParameter('dateTo', $toDate);
}
}
break;
}
}
}
// Apply sorting
$sortField = is_array($sort['sortBy']) ? ($sort['sortBy']['key'] ?? 'id') : ($sort['sortBy'] ?? 'id');
$sortDirection = ($sort['sortDesc'] ?? true) ? 'DESC' : 'ASC';
$queryBuilder->orderBy("d.$sortField", $sortDirection);
// Calculate total items
$totalItemsQuery = clone $queryBuilder;
$totalItems = $totalItemsQuery->select('COUNT(DISTINCT d.id)')
->getQuery()
->getSingleScalarResult();
// Apply pagination
$queryBuilder->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit);
$docs = $queryBuilder->getQuery()->getArrayResult();
$dataTemp = []; $dataTemp = [];
foreach ($data as $item) { foreach ($docs as $doc) {
$temp = [ $item = [
'id' => $item->getId(), 'id' => $doc['id'],
'dateSubmit' => $item->getDateSubmit(), 'dateSubmit' => $doc['dateSubmit'],
'date' => $item->getDate(), 'date' => $doc['date'],
'type' => $item->getType(), 'type' => $doc['type'],
'code' => $item->getCode(), 'code' => $doc['code'],
'des' => $item->getDes(), 'des' => $doc['des'],
'amount' => $item->getAmount(), 'amount' => $doc['amount'],
'submitter' => $item->getSubmitter()->getFullName(), 'submitter' => $doc['submitter'],
]; ];
if ($params['type'] == 'rfsell' || $params['type'] == 'rfbuy' || $params['type'] == 'buy' || $params['type'] == 'sell') {
$mainRow = $entityManager->getRepository(HesabdariRow::class)->getNotEqual($item, 'person'); // Get related person info if applicable
$temp['person'] = ''; if (in_array($doc['type'], ['rfsell', 'rfbuy', 'buy', 'sell'])) {
if ($mainRow) $personInfo = $entityManager->createQueryBuilder()
$temp['person'] = Explore::ExplorePerson($mainRow->getPerson()); ->select('p.id, p.nikename, p.code')
->from('App\Entity\HesabdariRow', 'r')
->join('r.person', 'p')
->where('r.doc = :docId')
->andWhere('r.person IS NOT NULL')
->setParameter('docId', $doc['id'])
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
$item['person'] = $personInfo ? [
'id' => $personInfo['id'],
'nikename' => $personInfo['nikename'],
'code' => $personInfo['code'],
] : null;
} }
$temp['label'] = null; // Get payment status
if ($item->getInvoiceLabel()) { $pays = $entityManager->createQueryBuilder()
$temp['label'] = [ ->select('SUM(rd.amount) as total_pays')
'code' => $item->getInvoiceLabel()->getCode(), ->from('App\Entity\HesabdariDoc', 'd')
'label' => $item->getInvoiceLabel()->getLabel() ->leftJoin('d.relatedDocs', 'rd')
]; ->where('d.id = :docId')
} ->setParameter('docId', $doc['id'])
//get status of doc ->getQuery()
$temp['status'] = 'تسویه نشده'; ->getSingleScalarResult();
$pays = 0;
foreach ($item->getRelatedDocs() as $relatedDoc) {
$pays += $relatedDoc->getAmount();
}
if ($item->getAmount() <= $pays)
$temp['status'] = 'تسویه شده';
$dataTemp[] = $temp; $item['status'] = ($pays && $pays >= $doc['amount']) ? 'تسویه شده' : 'تسویه نشده';
$dataTemp[] = $item;
} }
return $this->json($dataTemp);
return $this->json([
'items' => $dataTemp,
'total' => (int) $totalItems,
'page' => $page,
'limit' => $limit,
]);
} }
/** /**

View file

@ -943,9 +943,12 @@ class PersonsController extends AbstractController
], $acc['money']); ], $acc['money']);
} else { } else {
$transactions = []; $transactions = [];
if (is_array($params['items'])) {
foreach ($params['items'] as $param) { foreach ($params['items'] as $param) {
$id = is_array($param) ? ($param['id'] ?? null) : $param;
if ($id !== null) {
$prs = $entityManager->getRepository(HesabdariRow::class)->findByJoinMoney([ $prs = $entityManager->getRepository(HesabdariRow::class)->findByJoinMoney([
'id' => $param['id'], 'id' => $id,
'bid' => $acc['bid'], 'bid' => $acc['bid'],
'person' => $person, 'person' => $person,
'year' => $acc['year'], 'year' => $acc['year'],
@ -955,6 +958,8 @@ class PersonsController extends AbstractController
} }
} }
} }
}
}
$spreadsheet = new Spreadsheet(); $spreadsheet = new Spreadsheet();
$activeWorksheet = $spreadsheet->getActiveSheet(); $activeWorksheet = $spreadsheet->getActiveSheet();
$arrayEntity = [ $arrayEntity = [
@ -1011,9 +1016,12 @@ class PersonsController extends AbstractController
], $acc['money']); ], $acc['money']);
} else { } else {
$transactions = []; $transactions = [];
if (is_array($params['items'])) {
foreach ($params['items'] as $param) { foreach ($params['items'] as $param) {
$id = is_array($param) ? ($param['id'] ?? null) : $param;
if ($id !== null) {
$prs = $entityManager->getRepository(HesabdariRow::class)->findByJoinMoney([ $prs = $entityManager->getRepository(HesabdariRow::class)->findByJoinMoney([
'id' => $param['id'], 'id' => $id,
'bid' => $acc['bid'], 'bid' => $acc['bid'],
'person' => $person, 'person' => $person,
'year' => $acc['year'], 'year' => $acc['year'],
@ -1023,6 +1031,8 @@ class PersonsController extends AbstractController
} }
} }
} }
}
}
$pid = $provider->createPrint( $pid = $provider->createPrint(
$acc['bid'], $acc['bid'],
$this->getUser(), $this->getUser(),

View file

@ -0,0 +1,116 @@
{% extends "pdf/base.html.twig" %}
{% block body %}
<div style="width:100%; border:1px solid black;border-radius: 8px;margin-top:5px;text-align:center;">
<div class="tg-wrap" style="width:100%;border-radius: 8px 8px 0px 0px;text-align:center;background-color:gray">
<b style="color:white;">کارت حساب صندوق</b>
</div>
<table style="width:100%;">
<tbody>
<tr style="text-align:center;">
<td class="">
<p>
<b>نام:
</b>
{{ cashdesk.name }}
</p>
</td>
<td class="center">
<p>
<b>
کد حسابداری:
</b>
{{ cashdesk.code }}
</p>
</td>
<td class="center">
<p>
<b>شرح:
</b>
{{ cashdesk.des }}
</p>
</td>
</tr>
</tbody>
</table>
</div>
<div style="width:100%;margin-top:5px;text-align:center;">
<table style="width:100%;">
<tbody>
<tr style="text-align: center; background-color: grey; text-color: white">
<td style="width: 35px;">ردیف</td>
<td class="center item">فاکتور/سند</td>
<td class="center item">تاریخ</td>
<td class="center item">توضیحات</td>
<td class="center item">شرح سند</td>
<td class="center item">تفضیل</td>
<td class="center item">واریز</td>
<td class="center item">برداشت</td>
<td class="center item">سال مالی</td>
</tr>
{% set sumBs = 0 %}
{% set sumBd = 0 %}
{% for item in items %}
{% set sumBs = sumBs + item.bs %}
{% set sumBd = sumBd + item.bd %}
<tr class="stimol">
<td class="center item">{{ loop.index }}</td>
<td class="center item">{{ item.doc.code }}</td>
<td class="center item">{{ item.doc.date }}</td>
<td class="center item">{{ item.des }}</td>
<td class="center item">{{ item.doc.des }}</td>
<td class="center item">{{ item.ref.name }}</td>
<td class="center item">{{ item.bd | number_format }}</td>
<td class="center item">{{ item.bs | number_format }}</td>
<td class="center item">{{ item.year.label }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="width:100%; border:1px solid black;border-radius: 8px;margin-top:5px;text-align:center;">
<div class="tg-wrap" style="width:100%;border-radius: 8px 8px 0px 0px;text-align:center;background-color:gray">
<b style="color:white;">وضعیت حساب</b>
</div>
<table style="width:100%;">
<tbody>
<tr style="text-align:center;">
<td class="center">
<p>
<b>جمع برداشت:
</b>
{{ sumBs | number_format }}
</p>
</td>
<td class="center">
<p>
<b>جمع واریز:
</b>
{{ sumBd | number_format }}
</p>
</td>
<td class="center">
<p>
<b>تراز حساب:
</b>
<span>{{ (sumBs - sumBd) | abs |number_format }}</span>
</p>
</td>
<td class="center">
<p>
<b> وضعیت:
</b>
{% if sumBs > sumBd%}
برداشت
{% elseif sumBs == sumBd %}
تسویه شده
{% else %}
واریز
{% endif %}
</p>
</td>
</tr>
</tbody>
</table>
</div>
{% endblock %}

View file

@ -255,7 +255,7 @@
</h4> </h4>
<ul class=""> <ul class="">
<li class=""> <li class="">
{{note}} {{note | raw}}
</li> </li>
</ul> </ul>
{% endif %} {% endif %}

View file

@ -255,7 +255,7 @@
</h4> </h4>
<ul class=""> <ul class="">
<li class=""> <li class="">
{{note}} {{note | raw}}
</li> </li>
</ul> </ul>
{% endif %} {% endif %}

View file

@ -255,7 +255,7 @@
</h4> </h4>
<ul class=""> <ul class="">
<li class=""> <li class="">
{{note}} {{note | raw}}
</li> </li>
</ul> </ul>
{% endif %} {% endif %}

View file

@ -236,33 +236,33 @@
{% set originalPrice = item.bs + item.discount %} {% set originalPrice = item.bs + item.discount %}
{% set unitPrice = originalPrice / item.commodityCount %} {% set unitPrice = originalPrice / item.commodityCount %}
{% endif %} {% endif %}
{{ unitPrice|round|number_format }} {{ unitPrice|round|number_format }} {{ doc.money.shortName }}
{% else %} {% else %}
0 0 {{ doc.money.shortName }}
{% endif %} {% endif %}
</td> </td>
{% if printOptions.discountInfo %} {% if printOptions.discountInfo %}
<td class="center item"> <td class="center item">
{% if item.showPercentDiscount %} {% if item.showPercentDiscount %}
{{ item.discountPercent }}% {{ item.discountPercent }}%
({{ (item.bs * item.commodityCount * item.discountPercent / 100)|round|number_format }}) ({{ (item.bs * item.commodityCount * item.discountPercent / 100)|round|number_format }} {{ doc.money.shortName }})
{% else %} {% else %}
{{ item.discount|number_format }} {{ item.discount|number_format }} {{ doc.money.shortName }}
{% endif %} {% endif %}
</td> </td>
<td class="center item"> <td class="center item">
{% if item.showPercentDiscount %} {% if item.showPercentDiscount %}
{% set originalPrice = item.bs / (1 - (item.discountPercent / 100)) %} {% set originalPrice = item.bs / (1 - (item.discountPercent / 100)) %}
{{ originalPrice|round|number_format }} {{ originalPrice|round|number_format }} {{ doc.money.shortName }}
{% else %} {% else %}
{{ (item.bs + item.discount)|number_format }} {{ (item.bs + item.discount)|number_format }} {{ doc.money.shortName }}
{% endif %} {% endif %}
</td> </td>
{% endif %} {% endif %}
{% if printOptions.taxInfo %} {% if printOptions.taxInfo %}
<td class="center item">{{ item.tax | number_format}}</td> <td class="center item">{{ item.tax | number_format}} {{ doc.money.shortName }}</td>
{% endif %} {% endif %}
<td class="center item">{{ item.bs| number_format }}</td> <td class="center item">{{ item.bs| number_format }} {{ doc.money.shortName }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
@ -296,7 +296,7 @@
</h4> </h4>
<ul class=""> <ul class="">
<li class=""> <li class="">
{{note}} {{note | raw}}
</li> </li>
</ul> </ul>
{% endif %} {% endif %}
@ -306,25 +306,25 @@
<td class="item" style="width:15%;padding:1%"> <td class="item" style="width:15%;padding:1%">
<h4> <h4>
تخفیف: تخفیف:
{{discount | number_format}} {{discount | number_format}} {{ doc.money.shortName }}
</h4> </h4>
<h4> <h4>
مالیات: مالیات:
{{taxAll | number_format}} {{taxAll | number_format}} {{ doc.money.shortName }}
</h4> </h4>
<h4> <h4>
حمل و نقل: حمل و نقل:
{{transfer | number_format}} {{transfer | number_format}} {{ doc.money.shortName }}
</h4> </h4>
{% if doc.amount != (doc.amount + discount) %} {% if doc.amount != (doc.amount + discount) %}
<h4> <h4>
جمع بدون تخفیف: جمع بدون تخفیف:
{{ (doc.amount + discount) | number_format}} {{ (doc.amount + discount) | number_format}} {{ doc.money.shortName }}
</h4> </h4>
{% endif %} {% endif %}
<h4> <h4>
جمع کل: جمع کل:
{{ doc.amount | number_format }} {{ doc.amount | number_format }} {{ doc.money.shortName }}
</h4> </h4>
</td> </td>
</tr> </tr>

View file

@ -1,6 +1,6 @@
{ {
"name": "hesabix", "name": "hesabix",
"version": "0.48.0", "version": "0.49.8",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View file

@ -86,6 +86,11 @@ export default {
returnObject: { returnObject: {
type: Boolean, type: Boolean,
default: false default: false
},
tableType: {
type: String,
required: true,
validator: (value) => ['cost', 'income'].includes(value)
} }
}, },
data() { data() {
@ -135,7 +140,7 @@ export default {
async fetchTreeData() { async fetchTreeData() {
this.loading = true; this.loading = true;
try { try {
const response = await axios.get('/api/accounting/table/childs/cost'); const response = await axios.get(`/api/accounting/table/childs/${this.tableType}`);
this.treeItems = response.data; this.treeItems = response.data;
if (this.modelValue) { if (this.modelValue) {

View file

@ -100,6 +100,7 @@ const fa_lang = {
ultimate_package: 'بسته‌های نامحدود', ultimate_package: 'بسته‌های نامحدود',
sell_chart: "فروش هفته گذشته", sell_chart: "فروش هفته گذشته",
bankaccounts_transactions: "کارت حساب بانک", bankaccounts_transactions: "کارت حساب بانک",
cashdesk_transactions: "کارت حساب صندوق",
print_queue: "صف چاپ", print_queue: "صف چاپ",
open_balance: "تراز افتتاحیه", open_balance: "تراز افتتاحیه",
sell_invoices_long: "فاکتور‌های فروش", sell_invoices_long: "فاکتور‌های فروش",

View file

@ -18,7 +18,89 @@
</v-toolbar> </v-toolbar>
<v-text-field v-model="searchValue" prepend-inner-icon="mdi-magnify" density="compact" hide-details :rounded="false" <v-text-field v-model="searchValue" prepend-inner-icon="mdi-magnify" density="compact" hide-details :rounded="false"
placeholder="جست و جو ..."></v-text-field> placeholder="جست و جو ...">
<template v-slot:append-inner>
<v-menu :close-on-content-click="false">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" size="sm" color="primary">
<v-icon>mdi-filter</v-icon>
<v-tooltip activator="parent" :text="$t('dialog.filters')" location="bottom" />
</v-icon>
</template>
<v-list>
<v-list-subheader color="primary">
<v-icon>mdi-filter</v-icon>
{{ $t('dialog.filters') }}
</v-list-subheader>
<!-- فیلتر درختی حسابها -->
<v-list-item>
<v-list-item-title class="text-dark mb-2">
فیلتر حساب:
<v-btn
v-if="selectedAccountId"
size="small"
color="primary"
variant="text"
class="ms-2"
@click="resetAccountFilter"
>
<v-icon size="small" class="me-1">mdi-refresh</v-icon>
بازنشانی
</v-btn>
</v-list-item-title>
<hesabdari-tree-view
v-model="selectedAccountId"
:show-sub-tree="true"
:selectable-only="false"
@select="handleAccountSelect"
@account-selected="handleAccountSelected"
/>
</v-list-item>
<v-divider class="my-2"></v-divider>
<!-- فیلتر بازه زمانی -->
<v-list-item>
<v-list-item-title class="text-dark mb-2">
</v-list-item-title>
<v-row>
<v-col cols="12">
<v-checkbox
v-model="timeFilters.find(f => f.value === 'custom').checked"
label="بازه زمانی"
@change="handleCustomDateFilterChange"
hide-details
/>
</v-col>
<v-col cols="12" v-if="timeFilters.find(f => f.value === 'custom').checked">
<Hdatepicker
v-model="dateRange.from"
label="از تاریخ"
@update:model-value="handleDateRangeChange"
/>
</v-col>
<v-col cols="12" v-if="timeFilters.find(f => f.value === 'custom').checked">
<Hdatepicker
v-model="dateRange.to"
label="تا تاریخ"
@update:model-value="handleDateRangeChange"
/>
</v-col>
</v-row>
</v-list-item>
<!-- فیلترهای زمانی -->
<v-list-item v-for="(filter, index) in timeFilters.filter(f => f.value !== 'custom')" :key="index" class="text-dark">
<template v-slot:title>
<v-checkbox v-model="filter.checked" :label="filter.label" @change="applyTimeFilter(filter.value)"
hide-details />
</template>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-text-field>
<v-data-table :headers="headers" :items="filteredItems" :search="searchValue" :loading="loading" <v-data-table :headers="headers" :items="filteredItems" :search="searchValue" :loading="loading"
:header-props="{ class: 'custom-header' }" hover> :header-props="{ class: 'custom-header' }" hover>
@ -151,8 +233,11 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed, watch } from 'vue'
import axios from 'axios' import axios from 'axios'
import moment from 'jalali-moment'
import HesabdariTreeView from '@/components/forms/HesabdariTreeView.vue'
import Hdatepicker from '@/components/forms/Hdatepicker.vue'
const searchValue = ref('') const searchValue = ref('')
const loading = ref(true) const loading = ref(true)
@ -167,6 +252,22 @@ const snackbar = ref({
}) })
const plugins = ref({}) const plugins = ref({})
// فیلترهای زمانی
const timeFilters = ref([
{ label: 'امروز', value: 'today', checked: false },
{ label: 'این هفته', value: 'week', checked: false },
{ label: 'این ماه', value: 'month', checked: false },
{ label: 'بازه زمانی', value: 'custom', checked: false },
{ label: 'همه', value: 'all', checked: true },
])
const timeFilter = ref('all')
const selectedAccountId = ref(null)
const dateRange = ref({
from: moment().locale('fa').subtract(1, 'days').format('YYYY/MM/DD'),
to: moment().locale('fa').format('YYYY/MM/DD')
})
const headers = [ const headers = [
{ title: 'وضعیت', key: 'state', sortable: true }, { title: 'وضعیت', key: 'state', sortable: true },
{ title: 'عملیات', key: 'operation' }, { title: 'عملیات', key: 'operation' },
@ -181,12 +282,155 @@ const isPluginActive = (plugName) => {
return plugins.value[plugName] !== undefined return plugins.value[plugName] !== undefined
} }
// متدهای مدیریت فیلتر حساب
const handleAccountSelect = (account) => {
if (account) {
selectedAccountId.value = account.code
loadData()
}
}
const handleAccountSelected = (account) => {
if (account) {
loadData()
}
}
// متد ریست کردن فیلتر حساب
const resetAccountFilter = () => {
selectedAccountId.value = null
loadData()
}
// متدهای مدیریت فیلتر زمانی
const handleDateRangeChange = () => {
if (dateRange.value.from && dateRange.value.to) {
const fromDate = moment(dateRange.value.from, 'jYYYY/jMM/jDD').locale('fa')
const toDate = moment(dateRange.value.to, 'jYYYY/jMM/jDD').locale('fa')
if (fromDate.isAfter(toDate)) {
snackbar.value = {
show: true,
message: 'تاریخ شروع نمی‌تواند بعد از تاریخ پایان باشد',
color: 'error'
}
dateRange.value = {
from: null,
to: null
}
return
}
loadData()
}
}
const handleCustomDateFilterChange = (checked) => {
if (checked) {
timeFilters.value.forEach(filter => {
if (filter.value !== 'custom') {
filter.checked = false
}
})
timeFilter.value = 'custom'
dateRange.value = {
from: moment().locale('fa').subtract(1, 'days').format('YYYY/MM/DD'),
to: moment().locale('fa').format('YYYY/MM/DD')
}
loadData()
} else {
timeFilters.value.forEach(filter => {
filter.checked = filter.value === 'all'
})
timeFilter.value = 'all'
dateRange.value = {
from: null,
to: null
}
loadData()
}
}
const applyTimeFilter = (value) => {
timeFilters.value.forEach((filter) => {
filter.checked = filter.value === value
})
timeFilter.value = value
if (value !== 'custom') {
dateRange.value = {
from: null,
to: null
}
}
loadData()
}
const loadData = async () => { const loadData = async () => {
try { try {
loading.value = true
const filters = {}
if (searchValue.value.trim()) {
filters.search = { value: searchValue.value.trim() }
}
if (timeFilter.value) {
filters.timeFilter = timeFilter.value
if (timeFilter.value === 'custom' && dateRange.value.from && dateRange.value.to) {
const fromDate = moment(dateRange.value.from, 'jYYYY/jMM/jDD').locale('fa').format('YYYY/MM/DD')
const toDate = moment(dateRange.value.to, 'jYYYY/jMM/jDD').locale('fa').format('YYYY/MM/DD')
filters.date = {
from: fromDate,
to: toDate
}
} else {
const today = moment().locale('fa').format('YYYY/MM/DD')
switch (timeFilter.value) {
case 'today':
filters.date = {
from: today,
to: today
}
break
case 'week':
filters.date = {
from: moment().locale('fa').subtract(6, 'days').format('YYYY/MM/DD'),
to: today
}
break
case 'month':
filters.date = {
from: moment().locale('fa').startOf('jMonth').format('YYYY/MM/DD'),
to: today
}
break
}
}
}
if (selectedAccountId.value) {
filters.account = selectedAccountId.value
}
const response = await axios.post('/api/accounting/search', { const response = await axios.post('/api/accounting/search', {
type: 'all' type: 'all',
filters,
pagination: {
page: 1,
limit: 100
},
sort: {
sortBy: 'id',
sortDesc: true
}
}) })
items.value = response.data.map(item => ({
items.value = response.data.items.map(item => ({
...item, ...item,
amount: item.amount.toLocaleString(), amount: item.amount.toLocaleString(),
amountRaw: item.amount amountRaw: item.amount
@ -195,6 +439,11 @@ const loadData = async () => {
} catch (error) { } catch (error) {
console.error('Error loading data:', error) console.error('Error loading data:', error)
loading.value = false loading.value = false
snackbar.value = {
show: true,
message: 'خطا در بارگذاری داده‌ها: ' + (error.response?.data?.detail || error.message),
color: 'error'
}
} }
} }
@ -241,20 +490,17 @@ const confirmDelete = async () => {
deleteLoading.value = true deleteLoading.value = true
const response = await axios.delete(`/api/hesabdari/direct/doc/delete/${selectedItem.value.id}`) const response = await axios.delete(`/api/hesabdari/direct/doc/delete/${selectedItem.value.id}`)
if (response.data.success) { if (response.data.success) {
// حذف آیتم از لیست
const index = items.value.findIndex(item => item.id === selectedItem.value.id) const index = items.value.findIndex(item => item.id === selectedItem.value.id)
if (index !== -1) { if (index !== -1) {
items.value.splice(index, 1) items.value.splice(index, 1)
} }
deleteDialog.value = false deleteDialog.value = false
// نمایش پیام موفقیت
snackbar.value = { snackbar.value = {
show: true, show: true,
message: 'سند با موفقیت حذف شد', message: 'سند با موفقیت حذف شد',
color: 'success' color: 'success'
} }
} else { } else {
// نمایش پیام خطا
snackbar.value = { snackbar.value = {
show: true, show: true,
message: response.data.message || 'خطا در حذف سند', message: response.data.message || 'خطا در حذف سند',
@ -262,7 +508,6 @@ const confirmDelete = async () => {
} }
} }
} catch (error) { } catch (error) {
// نمایش پیام خطا
snackbar.value = { snackbar.value = {
show: true, show: true,
message: error.response?.data?.message || 'خطا در ارتباط با سرور', message: error.response?.data?.message || 'خطا در ارتباط با سرور',
@ -273,6 +518,13 @@ const confirmDelete = async () => {
} }
} }
// اضافه کردن watch برای تغییرات تاریخها
watch([() => dateRange.value.from, () => dateRange.value.to], () => {
if (timeFilter.value === 'custom') {
handleDateRangeChange()
}
}, { deep: true })
onMounted(() => { onMounted(() => {
loadData() loadData()
loadPlugins() loadPlugins()

View file

@ -1,24 +1,63 @@
<template> <template>
<div class="block block-content-full "> <v-toolbar color="toolbar" :title="$t('drawer.cashdesk_transactions')">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1"> <template v-slot:prepend>
<h3 class="block-title text-primary-dark"> <v-tooltip :text="$t('dialog.back')" location="bottom">
<button @click="$router.back()" type="button" <template v-slot:activator="{ props }">
class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning"> <v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
<i class="fa fw-bold fa-arrow-right"></i> icon="mdi-arrow-right" />
</button> </template>
تراکنش های صندوق </v-tooltip>
</h3> </template>
</div> <v-spacer></v-spacer>
<div class="block-content pt-1 pb-3"> <v-menu>
<div class="row"> <template v-slot:activator="{ props }">
<div class="col-sm-12 col-md-12 m-0 p-0"> <v-btn v-bind="props" icon="" color="red">
<div class="col-sm-12 col-md-6 mb-1"> <v-tooltip activator="parent" :text="$t('dialog.export_pdf')" location="bottom" />
<div class="card push"> <v-icon icon="mdi-file-pdf-box"></v-icon>
<div class="card-header border-bottom-0 bg-primary-dark text-light"> </v-btn>
<h3 class="block-title"> گردش حساب <small class="text-info-light">{{ selectedObjectItem.name }}</small> </template>
</h3> <v-list>
</div> <v-list-subheader color="primary">{{ $t('dialog.export_pdf') }}</v-list-subheader>
<div class="card-body"> <v-list-item class="text-dark" :title="$t('dialog.selected')" @click="print(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.selected_all')" @click="print(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all"></v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="" color="green">
<v-tooltip activator="parent" :text="$t('dialog.export_excel')" location="bottom" />
<v-icon icon="mdi-file-excel-box"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">{{ $t('dialog.export_excel') }}</v-list-subheader>
<v-list-item class="text-dark" :title="$t('dialog.selected')" @click="excellOutput(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.selected_all')" @click="excellOutput(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all"></v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
<v-row class="pa-1">
<v-col cols="12" sm="12" md="12">
<v-card :loading="loading">
<v-card-text>
<v-row class="">
<v-col cols="12" sm="12" md="12">
<small class="mb-2">صندوق</small> <small class="mb-2">صندوق</small>
<v-cob dir="rtl" :options="objectItems" label="name" v-model="selectedObjectItem" <v-cob dir="rtl" :options="objectItems" label="name" v-model="selectedObjectItem"
@option:selected="updateRoute(selectedObjectItem.code)"> @option:selected="updateRoute(selectedObjectItem.code)">
@ -26,64 +65,69 @@
نتیجهای یافت نشد! نتیجهای یافت نشد!
</template> </template>
</v-cob> </v-cob>
<hr /> </v-col>
<v-col cols="12" sm="12" md="4">
<div class="fw-bold mb-2">کد حسابداری: <small class="text-primary">{{ selectedObjectItem.code }}</small> <div class="fw-bold mb-2">کد حسابداری: <small class="text-primary">{{ selectedObjectItem.code }}</small>
</div> </div>
<div class="fw-bold mb-2">نام : <small class="text-primary">{{ selectedObjectItem.name }}</small></div> <div class="fw-bold mb-2">نام : <small class="text-primary">{{ selectedObjectItem.name }}</small></div>
</v-col>
<v-col cols="12" sm="12" md="4">
<div class="fw-bold mb-2">شرح: <small class="text-primary">{{ selectedObjectItem.des }}</small></div> <div class="fw-bold mb-2">شرح: <small class="text-primary">{{ selectedObjectItem.des }}</small></div>
</div> </v-col>
</div> </v-row>
</v-card-text>
</div> </v-card>
</div> </v-col>
</div> <v-col cols="12" sm="12" md="12">
<div class="row"> <v-text-field :loading="loading" color="green" class="mb-0 pt-0 rounded-0" hide-details="auto" density="compact"
<div class="col-sm-12 col-md-12"> :placeholder="$t('dialog.search_txt')" v-model="searchValue" type="text" clearable>
<h3>تراکنش ها:</h3> <template v-slot:prepend-inner>
<div class="mb-1"> <v-tooltip location="bottom" :text="$t('dialog.search')">
<div class="input-group input-group-sm"> <template v-slot:activator="{ props }">
<span class="input-group-text"><i class="fa fa-search"></i></span> <v-icon v-bind="props" color="danger" icon="mdi-magnify"></v-icon>
<input v-model="searchValue" class="form-control" type="text" placeholder="جست و جو ..."> </template>
</div> </v-tooltip>
</div> </template>
<EasyDataTable table-class-name="customize-table" show-index alternating :search-value="searchValue" :headers="headers" :items="items" </v-text-field>
theme-color="#1d90ff" header-text-direction="center" body-text-direction="center" <EasyDataTable table-class-name="customize-table" show-index alternating v-model:items-selected="itemsSelected"
rowsPerPageMessage="تعداد سطر" emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از" :search-value="searchValue" :headers="headers" :items="items" theme-color="#1d90ff"
:loading="loading"> header-text-direction="center" body-text-direction="center" rowsPerPageMessage="تعداد سطر"
emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از" :loading="loading">
<template #item-operation="{ code }"> <template #item-operation="{ code }">
<router-link class="text-success" :to="'/acc/accounting/view/' + code"> <router-link class="text-success" :to="'/acc/accounting/view/' + code">
<i class="fa fa-eye px-1"></i> <i class="fa fa-eye px-1"></i>
</router-link> </router-link>
</template> </template>
</EasyDataTable> </EasyDataTable>
</div> </v-col>
</div> </v-row>
</div>
</div>
</template> </template>
<script> <script>
import axios from "axios"; import axios from "axios";
import { ref } from "vue"; import Swal from "sweetalert2";
export default { export default {
name: "card", name: "card",
data: () => { data: () => {
return { return {
searchValue: '', searchValue: '',
objectItems: [{ itemsSelected: [],
name: ''
}],
selectedObjectItem: {},
items: [], items: [],
loading: ref(true), selectedObjectItem: {
id: '',
code: 0,
name: '',
},
objectItems: [],
loading: true,
headers: [ headers: [
{ text: "عملیات", value: "operation" }, { text: "عملیات", value: "operation" },
{ text: "تاریخ", value: "date", 'sortable': true }, { text: "تاریخ", value: "date", 'sortable': true },
{ text: "شرح", value: "des" }, { text: "شرح", value: "des" },
{ text: "تفضیل", value: "ref", 'sortable': true }, { text: "تفضیل", value: "ref", 'sortable': true },
{ text: "بدهکار", value: "bd", 'sortable': true }, { text: "واریز", value: "bd", 'sortable': true },
{ text: ستانکار", value: "bs", 'sortable': true }, { text: رداشت", value: "bs", 'sortable': true },
] ]
} }
}, },
@ -96,7 +140,9 @@ export default {
this.loadData(); this.loadData();
}, },
loadData() { loadData() {
this.loading = true;
axios.post('/api/cashdesk/list').then((response) => { axios.post('/api/cashdesk/list').then((response) => {
this.loading = false;
this.objectItems = response.data; this.objectItems = response.data;
if (this.$route.params.id != '') { if (this.$route.params.id != '') {
this.loadObject(this.$route.params.id); this.loadObject(this.$route.params.id);
@ -126,7 +172,98 @@ export default {
}) })
this.loading = false; this.loading = false;
}); });
},
excellOutput(AllItems = true) {
if (AllItems) {
this.loading = true;
axios({
method: 'post',
url: '/api/cashdesk/card/list/excel',
data: { 'code': this.selectedObjectItem.code },
responseType: 'arraybuffer',
}).then((response) => {
this.loading = false;
var FILE = window.URL.createObjectURL(new Blob([response.data]));
var fileURL = window.URL.createObjectURL(new Blob([response.data]));
var fileLink = document.createElement('a');
fileLink.href = fileURL;
fileLink.setAttribute('download', 'کارت حساب صندوق ' + this.selectedObjectItem.name + '.xlsx');
document.body.appendChild(fileLink);
fileLink.click();
})
} }
else {
if (this.itemsSelected.length === 0) {
Swal.fire({
text: 'هیچ آیتمی انتخاب نشده است.',
icon: 'info',
confirmButtonText: 'قبول'
});
}
else {
this.loading = true;
axios({
method: 'post',
url: '/api/cashdesk/card/list/excel',
responseType: 'arraybuffer',
data: {
'code': this.selectedObjectItem.code,
'items': this.itemsSelected
}
}).then((response) => {
this.loading = false;
var FILE = window.URL.createObjectURL(new Blob([response.data]));
var fileURL = window.URL.createObjectURL(new Blob([response.data]));
var fileLink = document.createElement('a');
fileLink.href = fileURL;
fileLink.setAttribute('download', 'کارت حساب صندوق ' + this.selectedObjectItem.name + '.xlsx');
document.body.appendChild(fileLink);
fileLink.click();
})
}
}
},
print(AllItems = true) {
if (this.selectedObjectItem == null) {
Swal.fire({
text: 'هیچ آیتمی انتخاب نشده است.',
icon: 'info',
confirmButtonText: 'قبول'
});
}
else {
if (AllItems) {
this.loading = true;
axios.post('/api/cashdesk/card/list/print', { 'code': this.selectedObjectItem.code }).then((response) => {
this.printID = response.data.id;
this.loading = false;
window.open(this.$API_URL + '/front/print/' + this.printID, '_blank', 'noreferrer');
})
}
else {
if (this.itemsSelected.length === 0) {
Swal.fire({
text: 'هیچ آیتمی انتخاب نشده است.',
icon: 'info',
confirmButtonText: 'قبول'
});
}
else {
this.loading = true;
axios.post('/api/cashdesk/card/list/print', {
'code': this.selectedObjectItem.code,
'items': this.itemsSelected
}).then((response) => {
this.loading = false;
this.printID = response.data.id;
window.open(this.$API_URL + '/front/print/' + this.printID, '_blank', 'noreferrer');
})
}
}
}
},
} }
} }
</script> </script>

View file

@ -1,147 +1,329 @@
<template> <template>
<div class="block block-content-full "> <v-toolbar color="toolbar" :title="$t('drawer.cashdesks')">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1"> <template v-slot:prepend>
<h3 class="block-title text-primary-dark"> <v-tooltip :text="$t('dialog.back')" location="bottom">
<button @click="$router.back()" type="button" class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning"> <template v-slot:activator="{ props }">
<i class="fa fw-bold fa-arrow-right"></i> <v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" icon="mdi-arrow-right" />
</button> </template>
<i class="fa fa-bank px-2"></i> </v-tooltip>
صندوق ها </h3> </template>
<div class="block-options"> <v-spacer />
<router-link to="/acc/cashdesk/mod/" class="block-options-item">
<span class="fa fa-plus fw-bolder"></span> <v-slide-group show-arrows>
</router-link> <v-slide-group-item>
</div> <v-tooltip :text="$t('dialog.add_new')" location="bottom">
</div> <template v-slot:activator="{ props }">
<div class="block-content pt-1 pb-3"> <v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/cashdesk/mod/" />
<div class="row"> </template>
<div class="col-sm-12 col-md-12 m-0 p-0"> </v-tooltip>
<div class="mb-1"> </v-slide-group-item>
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="fa fa-search"></i></span> <v-slide-group-item>
<input v-model="searchValue" class="form-control" type="text" placeholder="جست و جو ..."> <v-tooltip :text="$t('dialog.column_settings')" location="bottom">
</div> <template v-slot:activator="{ props }">
</div> <v-btn v-bind="props" icon="mdi-table-cog" color="primary" @click="showColumnDialog = true" />
<EasyDataTable table-class-name="customize-table" </template>
show-index </v-tooltip>
alternating </v-slide-group-item>
:search-value="searchValue" </v-slide-group>
:headers="headers" </v-toolbar>
:items="items"
theme-color="#1d90ff" <v-text-field
header-text-direction="center" v-model="search"
body-text-direction="center"
rowsPerPageMessage="تعداد سطر"
emptyMessage="اطلاعاتی برای نمایش وجود ندارد"
rowsOfPageSeparatorMessage="از"
:loading="loading" :loading="loading"
color="green"
class="mb-0 pt-0 rounded-0"
hide-details="auto"
density="compact"
:rounded="false"
:placeholder="$t('dialog.search_txt')"
clearable
> >
<template #item-operation="{ code }"> <template v-slot:prepend-inner>
<button aria-expanded="false" aria-haspopup="true" class="btn btn-sm btn-link" <v-tooltip location="bottom" :text="$t('dialog.search')">
data-bs-toggle="dropdown" id="dropdown-align-center-alt-primary" type="button"> <template v-slot:activator="{ props }">
<i class="fa-solid fa-ellipsis"></i> <v-icon v-bind="props" color="danger" icon="mdi-magnify" />
</button>
<div aria-labelledby="dropdown-align-center-outline-primary" class="dropdown-menu dropdown-menu-end"
style="">
<router-link class="dropdown-item" :to="'/acc/cashdesk/card/view/' + code">
<i class="fa fa-eye text-success pe-2"></i>
مشاهده
</router-link>
<router-link class="dropdown-item" :to="'/acc/cashdesk/mod/' + code">
<i class="fa fa-edit pe-2"></i>
ویرایش
</router-link>
<button type="button" @click="deleteItem(code)" class="dropdown-item text-danger">
<i class="fa fa-trash pe-2"></i>
حذف
</button>
</div>
</template> </template>
<template #item-name="{ name,code }"> </v-tooltip>
<router-link :to="'/acc/cashdesk/card/view/' + code"> </template>
{{name}} </v-text-field>
<v-data-table
:headers="visibleHeaders"
:items="items"
:loading="loading"
:search="search"
class="elevation-1 text-center"
:header-props="{ class: 'custom-header' }"
>
<template v-slot:item="{ item }">
<tr>
<td v-if="isColumnVisible('operation')" class="text-center">
<v-menu>
<template v-slot:activator="{ props }">
<v-btn variant="text" size="small" color="error" icon="mdi-menu" v-bind="props" />
</template>
<v-list>
<v-list-item :to="'/acc/cashdesk/card/view/' + item.code">
<template v-slot:prepend>
<v-icon color="success" icon="mdi-eye" />
</template>
<v-list-item-title>{{ $t('dialog.view') }}</v-list-item-title>
</v-list-item>
<v-list-item :to="'/acc/cashdesk/mod/' + item.code">
<template v-slot:prepend>
<v-icon icon="mdi-pencil" />
</template>
<v-list-item-title>{{ $t('dialog.edit') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="confirmDelete(item.code)">
<template v-slot:prepend>
<v-icon color="error" icon="mdi-delete" />
</template>
<v-list-item-title>{{ $t('dialog.delete') }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</td>
<td v-if="isColumnVisible('code')" class="text-center">{{ formatNumber(item.code) }}</td>
<td v-if="isColumnVisible('name')" class="text-center">
<router-link :to="'/acc/cashdesk/card/view/' + item.code">
{{ item.name }}
</router-link> </router-link>
</td>
<td v-if="isColumnVisible('balance')" class="text-center">
<span :class="Number(item.balance) >= 0 ? 'text-success' : 'text-error'">
{{ formatNumber(Math.abs(Number(item.balance))) }}
<span v-if="Number(item.balance) < 0">منفی</span>
</span>
</td>
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
</tr>
</template> </template>
<template #item-balance="{ balance }"> </v-data-table>
<label class="text-success" v-if="balance >= 0">{{ $filters.formatNumber(balance)}}</label>
<label class="text-danger" v-else>{{ $filters.formatNumber( -1 * balance ) }} منفی</label> <v-dialog v-model="showColumnDialog" max-width="500">
</template> <v-card>
</EasyDataTable> <v-toolbar color="toolbar" :title="$t('dialog.manage_columns')">
</div> <v-spacer></v-spacer>
</div> <v-btn icon @click="showColumnDialog = false">
</div> <v-icon>mdi-close</v-icon>
</div> </v-btn>
</v-toolbar>
<v-card-text>
<v-row>
<v-col v-for="header in allHeaders" :key="header.key" cols="12" sm="6">
<v-checkbox
v-model="header.visible"
:label="header.title"
@change="updateColumnVisibility"
hide-details
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="deleteDialog.show" max-width="400">
<v-card>
<v-card-title class="text-h6">
تأیید حذف
</v-card-title>
<v-card-text>
آیا برای حذف صندوق مطمئن هستید؟
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="deleteDialog.show = false">خیر</v-btn>
<v-btn color="error" variant="text" @click="deleteItem">بله</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="messageDialog.show" max-width="400">
<v-card>
<v-card-title :class="messageDialog.color + ' text-h6'">
{{ messageDialog.title }}
</v-card-title>
<v-card-text class="pt-4">
{{ messageDialog.message }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="messageDialog.show = false">قبول</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template> </template>
<script> <script setup>
import axios from "axios"; import { ref, computed, onMounted } from 'vue';
import Swal from "sweetalert2"; import axios from 'axios';
import {ref} from "vue";
export default { // Refs
name: "list", const loading = ref(false);
data: ()=>{return { const items = ref([]);
searchValue: '', const search = ref('');
loading: ref(true), const showColumnDialog = ref(false);
items:[],
headers: [ // دیالوگها
{ text: "عملیات", value: "operation", width: "130"}, const deleteDialog = ref({
{ text: "کد", value: "code", width: "100px" }, show: false,
{ text: "نام صندوق", value: "name", width: "120px"}, code: null
{ text: "موجودی()", value: "balance", width: "140px"}, });
{ text: "توضیحات", value: "des", width: "150px"},
] const messageDialog = ref({
}}, show: false,
methods: { title: '',
loadData(){ message: '',
axios.post('/api/cashdesk/list') color: 'primary'
.then((response)=>{ });
this.items = response.data;
this.loading = false; // تابع فرمتکننده اعداد
}) const formatNumber = (value) => {
}, if (!value) return '0';
deleteItem(code) { return Number(value).toLocaleString('fa-IR');
Swal.fire({ };
text: 'آیا برای حذف صندوق مطمئن هستید؟',
showCancelButton: true, // تعریف همه ستونها با align مرکز
confirmButtonText: 'بله', const allHeaders = ref([
cancelButtonText: `خیر`, { title: "عملیات", key: "operation", align: 'center', sortable: false, width: 100, visible: true },
}).then((result) => { { title: "کد", key: "code", align: 'center', sortable: true, width: 100, visible: true },
/* Read more about isConfirmed, isDenied below */ { title: "نام صندوق", key: "name", align: 'center', sortable: true, width: 140, visible: true },
if (result.isConfirmed) { { title: "موجودی", key: "balance", align: 'center', sortable: true, width: 140, visible: true },
axios.post('/api/cashdesk/delete/' + code).then((response) => { { title: "توضیحات", key: "des", align: 'center', sortable: true, width: 150, visible: true },
if (response.data.result == 1) { ]);
let index = 0;
for (let z = 0; z < this.items.length; z++) { // ستونهای قابل نمایش
index++; const visibleHeaders = computed(() => {
if (this.items[z]['code'] == code) { return allHeaders.value.filter(header => header.visible);
this.items.splice(index - 1, 1); });
}
} // بررسی نمایش ستون
Swal.fire({ const isColumnVisible = (key) => {
text: 'صندوق با موفقیت حذف شد.', return allHeaders.value.find(header => header.key === key)?.visible;
icon: 'success', };
confirmButtonText: 'قبول'
// کلید ذخیرهسازی در localStorage
const LOCAL_STORAGE_KEY = 'hesabix_cashdesk_table_columns';
// لود تنظیمات ستونها
const loadColumnSettings = () => {
const savedSettings = localStorage.getItem(LOCAL_STORAGE_KEY);
if (savedSettings) {
const visibleColumns = JSON.parse(savedSettings);
allHeaders.value.forEach(header => {
header.visible = visibleColumns.includes(header.key);
}); });
} }
else if (response.data.result == 2) { };
Swal.fire({
text: 'صندوق به دلیل داشتن تراکنش و اسناد حسابداری مرتبط قابل حذف نیست.', // ذخیره تنظیمات ستونها
icon: 'error', const updateColumnVisibility = () => {
confirmButtonText: 'قبول' const visibleColumns = allHeaders.value
.filter(header => header.visible)
.map(header => header.key);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(visibleColumns));
};
// نمایش پیام
const showMessage = (message, title = 'پیام', color = 'primary') => {
messageDialog.value = {
show: true,
title,
message,
color
};
};
// تأیید حذف
const confirmDelete = (code) => {
deleteDialog.value = {
show: true,
code
};
};
// بارگذاری دادهها
const loadData = async () => {
loading.value = true;
try {
const response = await axios.post('/api/cashdesk/list');
items.value = response.data;
} catch (error) {
console.error('Error loading data:', error);
showMessage('خطا در بارگذاری داده‌ها: ' + error.message, 'خطا', 'error');
} finally {
loading.value = false;
}
};
// حذف آیتم
const deleteItem = async () => {
const code = deleteDialog.value.code;
deleteDialog.value.show = false;
if (!code) return;
try {
loading.value = true;
const response = await axios.post(`/api/cashdesk/delete/${code}`);
if (response.data.result === 1) {
items.value = items.value.filter(item => item.code !== code);
showMessage('صندوق با موفقیت حذف شد.', 'موفقیت', 'success');
} else if (response.data.result === 2) {
showMessage('صندوق به دلیل داشتن تراکنش و اسناد حسابداری مرتبط قابل حذف نیست.', 'خطا', 'error');
}
} catch (error) {
console.error('Error deleting item:', error);
showMessage('خطا در حذف آیتم: ' + error.message, 'خطا', 'error');
} finally {
loading.value = false;
}
};
// مانت کامپوننت
onMounted(() => {
loadColumnSettings();
loadData();
}); });
}
})
}
})
}
},
beforeMount() {
this.loadData();
}
}
</script> </script>
<style scoped> <style>
.v-data-table {
width: 100%;
overflow-x: auto;
}
/* استایل برای وسط‌چین کردن همه سلول‌های جدول */
:deep(.v-data-table-header th) {
text-align: center !important;
}
:deep(.v-data-table__wrapper table td) {
text-align: center !important;
}
/* استایل برای رنگ‌های متن */
.text-success {
color: #4caf50 !important;
}
.text-error {
color: #ff5252 !important;
}
/* استایل برای لینک‌ها در جدول */
:deep(.v-data-table__wrapper table td a) {
text-decoration: none;
color: #1976d2;
}
:deep(.v-data-table__wrapper table td a:hover) {
text-decoration: underline;
}
</style> </style>

View file

@ -103,7 +103,7 @@
<v-card-text> <v-card-text>
<v-row> <v-row>
<v-col cols="12" md="4"> <v-col cols="12" md="4">
<Htabletreeselect v-model="item.id" :items="listscosts" label="مرکز هزینه" /> <Htabletreeselect v-model="item.id" label="مرکز هزینه" tableType="cost" />
</v-col> </v-col>
<v-col cols="12" md="4"> <v-col cols="12" md="4">
<Hnumberinput <Hnumberinput
@ -360,7 +360,6 @@ export default {
sum: 0, sum: 0,
balance: 0, balance: 0,
listPersons: [], listPersons: [],
listscosts: [],
listBanks: [], listBanks: [],
listCashdesks: [], listCashdesks: [],
listSalarys: [], listSalarys: [],
@ -398,12 +397,7 @@ export default {
beforeRouteUpdate(to, from) { beforeRouteUpdate(to, from) {
this.loadData(to.params.id); this.loadData(to.params.id);
}, },
computed: {
formattedCostItems() {
// تبدیل ساختار درختی به آرایه ساده
return this.flattenCostItems(this.listscosts);
}
},
methods: { methods: {
calc() { calc() {
this.sum = 0; this.sum = 0;
@ -553,10 +547,6 @@ export default {
this.data.date = response.data.now; this.data.date = response.data.now;
}) })
} }
//get list of items
axios.post('/api/accounting/table/childs/cost').then((response) => {
this.listscosts = response.data;
});
//get list of banks //get list of banks
axios.post('/api/bank/list').then((response) => { axios.post('/api/bank/list').then((response) => {

View file

@ -1,363 +1,329 @@
<template> <template>
<div class="block block-content-full "> <v-container fluid class="pa-0">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1"> <!-- هدر -->
<h3 class="block-title text-primary-dark"> <v-toolbar color="toolbar" title="درآمد" flat>
<router-link class="text-warning mx-2 px-2" to="/acc/incomes/list"> <template v-slot:prepend>
<i class="fa fw-bold fa-arrow-right"></i> <v-tooltip :text="$t('dialog.back')" location="bottom">
</router-link> <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" />
</h3>
<div class="block-options">
<archive-upload v-if="this.$route.params.id != ''" :docid="this.$route.params.id" doctype="income" cat="income"></archive-upload>
<button :disabled="this.canSubmit != true" @click="save()" type="button" class="btn btn-sm btn-alt-primary">
<i class="fa fa-save"></i>
ثبت
</button>
</div>
</div>
<div class="block-content py-3 px-0 vl-parent">
<loading color="blue" loader="dots" v-model:active="isLoading" :is-full-page="false" />
<div class="container">
<div class="row">
<div class="col-sm-12 col-md-6 mb-2">
<date-picker class="form-control" v-model="data.date" format="jYYYY/jMM/jDD" display-format="jYYYY/jMM/jDD"
:min="year.start" :max="year.end" />
</div>
<div class="col-sm-12 col-md-6 mb-2">
<div class="alert alert-sm alert-info">
<i class="fa fa-info-circle me-2"></i>
دکمه ثبت بعد از صفر بودن مبلغ باقی مانده و تکمیل شدن حسابها فعال می شود
</div>
</div>
<div class="col-sm-12 col-md-12">
<div class="form-floating mb-2">
<input v-model="data.des" class="form-control" type="text">
<label class="form-label">شرح</label>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-6 px-1" v-for="(item, index) in incomes">
<div class="block block-rounded border border-gray">
<div class="block-header bg-default-dark">
<h3 class="block-title">
<small class="text-white">
<span class="text-danger mx-2">{{ index + 1 }}</span>
<i class="fa fa-ticket"></i>
مرکز درآمد
</small>
</h3>
<span class="block-options">
<button title="حذف" class="btn-block-option text-white ps-2" @click="removeItem(index)">
<i class="fa fa-trash"></i>
</button>
</span>
</div>
<div class="block-content-sm mx-2">
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="row">
<div class="col-sm-12 col-md-12">
<small class="mb-2">مرکز درآمد</small>
<treeselect :disable-branch-nodes="true" v-model="item.id" :multiple="false"
:options="listIncomes" placeholder="انتخاب مرکز درآمد" noOptionsText="آیتمی انتخاب نشده است."
noChildrenText="فاقد زیرمجموعه" noResultsText="نتیجه‌ای یافت نشد" />
</div>
<div class="col-sm-12 col-md-12">
<small class="mb-2">مبلغ</small>
<money3 @change="calc()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
</money3>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-floating my-2">
<input v-model="item.des" type="text" class="form-control">
<label>شرح</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-12 col-md-6 px-1" v-for="(item, index) in banks">
<div class="block block-rounded border border-gray">
<div class="block-header bg-warning">
<h3 class="block-title">
<small class="text-black">
<span class="mx-2">{{ index + 1 }}</span>
<i class="fa fa-bank"></i>
حساب بانکی
</small>
</h3>
<span class="block-options">
<button title="حذف" class="btn-block-option text-white ps-2" @click="removeBank(index)">
<i class="fa fa-trash"></i>
</button>
</span>
</div>
<div class="block-content-sm mx-2">
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="row">
<div class="col-sm-12 col-md-12">
<small class="mb-2">بانک</small>
<v-cob dir="rtl" :options="listBanks" label="name" v-model="item.id"
@option:deselecting="funcCanSubmit()" @search:focus="funcCanSubmit()"
@option:selecting="funcCanSubmit()">
<template #no-options="{ search, searching, loading }">
نتیجهای یافت نشد!
</template> </template>
</v-cob> </v-tooltip>
</div>
<div class="col-sm-12 col-md-12">
<small class="mb-2">مبلغ</small>
<money3 @change="calc()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
</money3>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-floating my-2">
<input v-model="item.des" type="text" class="form-control">
<label>شرح</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-12 col-md-6 px-1" v-for="(item, index) in salarys">
<div class="block block-rounded border border-gray">
<div class="block-header bg-info">
<h3 class="block-title">
<small class="text-black">
<span class="mx-2">{{ index + 1 }}</span>
<i class="fa fa-dot-circle"></i>
تنخواه گردان
</small>
</h3>
<span class="block-options">
<button title="حذف" class="btn-block-option text-white ps-2" @click="removeSalary(index)">
<i class="fa fa-trash"></i>
</button>
</span>
</div>
<div class="block-content-sm mx-2">
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="row">
<div class="col-sm-12 col-md-12">
<small class="mb-2">تنخواه گردان</small>
<v-cob @change="alert()" dir="rtl" :options="listSalarys" label="name" v-model="item.id"
@option:deselecting="funcCanSubmit()" @search:focus="funcCanSubmit()"
@option:selecting="funcCanSubmit()">
<template #no-options="{ search, searching, loading }">
نتیجهای یافت نشد!
</template> </template>
</v-cob> <v-spacer></v-spacer>
</div>
<div class="col-sm-12 col-md-12">
<small class="mb-2">مبلغ</small>
<money3 @change="calc()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
</money3>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-floating my-2">
<input v-model="item.des" type="text" class="form-control">
<label>شرح</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-12 col-md-6 px-1" v-for="(item, index) in cashdesks">
<div class="block block-rounded border border-gray">
<div class="block-header bg-light">
<h3 class="block-title">
<small class="text-black">
<span class="mx-2">{{ index + 1 }}</span>
<i class="fa fa-money-bill-wheat"></i>
صندوق
</small>
</h3>
<span class="block-options">
<button title="حذف" class="btn-block-option text-danger ps-2" @click="removeCashdesk(index)">
<i class="fa fa-trash"></i>
</button>
</span>
</div>
<div class="block-content-sm mx-2">
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="row">
<div class="col-sm-12 col-md-12">
<small class="mb-2">صندوق</small>
<v-cob @change="alert()" dir="rtl" :options="listCashdesks" label="name" v-model="item.id"
@option:deselecting="funcCanSubmit()" @search:focus="funcCanSubmit()"
@option:selecting="funcCanSubmit()">
<template #no-options="{ search, searching, loading }">
نتیجهای یافت نشد!
</template>
</v-cob>
</div>
<div class="col-sm-12 col-md-12">
<small class="mb-2">مبلغ</small>
<money3 @change="calc()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
</money3>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-floating my-2">
<input v-model="item.des" type="text" class="form-control">
<label>شرح</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-12 col-md-6 px-1" v-for="(item, index) in persons">
<div class="block block-rounded border border-gray">
<div class="block-header bg-light">
<h3 class="block-title">
<small class="text-black">
<span class="mx-2">{{ index + 1 }}</span>
<i class="fa fa-person"></i>
شخص
</small>
</h3>
<span class="block-options">
<quickAdd :code="0"></quickAdd>
<button title="حذف" class="btn-block-option text-danger ps-2" @click="removePerson(index)">
<i class="fa fa-trash"></i>
</button>
</span>
</div>
<div class="block-content-sm mx-2">
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="row">
<div class="col-sm-12 col-md-12">
<small class="mb-2">شخص</small>
<v-cob :filterable="false" @search="searchPerson" class="" dir="rtl" :options="listPersons" <v-menu>
label="nikename" v-model="item.id"> <template v-slot:activator="{ props }">
<template v-slot:option="option"> <v-btn color="error" v-bind="props">
<div class="row mb-1"> افزودن حساب
<div class="col-12"> <v-icon end>mdi-chevron-down</v-icon>
<i class="fa fa-user me-2"></i> </v-btn>
{{ option.nikename }}
</div>
<div class="col-12">
<div class="row">
<div v-if="option.mobile != ''" class="col-6">
<i class="fa fa-phone me-2"></i>
{{ option.mobile }}
</div>
<div class="col-6" v-if="parseInt(option.bs) - parseInt(option.bd) != 0">
<i class="fa fa-bars"></i>
تراز:
{{ $filters.formatNumber(Math.abs(parseInt(option.bs) -
parseInt(option.bd))) }}
<span class="" v-if="parseInt(option.bs) - parseInt(option.bd) < 0">
بدهکار </span>
<span class="" v-if="parseInt(option.bs) - parseInt(option.bd) > 0">
بستانکار </span>
</div>
</div>
</div>
</div>
</template> </template>
</v-cob> <v-list>
</div> <v-list-item @click="addItem()">
<div class="col-sm-12 col-md-12"> <template v-slot:prepend>
<small class="mb-2">مبلغ</small> <v-icon>mdi-plus</v-icon>
<money3 @change="calc()" class="form-control" v-model="item.amount" v-bind="currencyConfig"> </template>
</money3> <v-list-item-title>مرکز درآمد</v-list-item-title>
</div> </v-list-item>
</div> <v-list-item @click="addBank()">
</div> <template v-slot:prepend>
</div> <v-icon>mdi-bank</v-icon>
<div class="row"> </template>
<div class="col-sm-12 col-md-12"> <v-list-item-title>حساب بانکی</v-list-item-title>
<div class="form-floating my-2"> </v-list-item>
<input v-model="item.des" type="text" class="form-control"> <v-list-item @click="addCashdesk()">
<label>شرح</label> <template v-slot:prepend>
</div> <v-icon>mdi-cash-register</v-icon>
</div> </template>
</div> <v-list-item-title>صندوق</v-list-item-title>
</div> </v-list-item>
</div> <v-list-item @click="addSalary()">
</div> <template v-slot:prepend>
</div> <v-icon>mdi-wallet</v-icon>
<div class="row"> </template>
<div class="col-12 text-end"> <v-list-item-title>تنخواه گردان</v-list-item-title>
<div class="row"> </v-list-item>
<div class="col-6 text-end"> <v-list-item @click="addPerson()">
<button @click="addItem()" class="btn btn-primary mx-1"> <template v-slot:prepend>
<i class="fa fa-plus"></i> <v-icon>mdi-account</v-icon>
افزودن آیتم </template>
</button> <v-list-item-title>شخص</v-list-item-title>
</div> </v-list-item>
<div class="col-6 text-start"> </v-list>
<div class="dropdown dropup"> </v-menu>
<button aria-expanded="false" aria-haspopup="true" class="btn btn-danger dropdown-toggle" <archive-upload v-if="$route.params.id" :docid="$route.params.id" doctype="income" cat="income" />
data-bs-toggle="dropdown" id="dropdown-dropup-secondary" type="button"> افزودن حساب </button>
<div aria-labelledby="dropdown-dropup-secondary" class="border border-danger dropdown-menu" style=""> <v-tooltip :text="$t('dialog.save')" location="bottom">
<button @click="addBank()" type="button" class="dropdown-item"> <template v-slot:activator="{ props }">
<i class="fa fa-bank"></i> <v-btn v-bind="props" color="primary" :disabled="!canSubmit" @click="save()" class="ms-2" icon="mdi-content-save" />
</template>
</v-tooltip>
</v-toolbar>
<!-- محتوا -->
<v-container>
<v-row>
<v-col cols="12" md="6">
<Hdatepicker v-model="data.date" label="تاریخ" />
</v-col>
<v-col cols="12" md="6">
<v-text-field v-model="data.des" label="شرح" variant="outlined"></v-text-field>
</v-col>
</v-row>
<v-card color="error" variant="outlined" class="mb-4 mt-2">
<v-card-text>
<v-row>
<v-col cols="6">
مجموع: {{ $filters.formatNumber(sum) }}
</v-col>
<v-col cols="6">
باقیمانده: {{ $filters.formatNumber(balance) }}
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- مرکز درآمد -->
<template v-for="(item, index) in incomes" :key="'income'+index">
<v-card class="mb-4">
<v-toolbar color="primary" density="compact">
<v-toolbar-title class="text-white text--secondary">
<span class="text-error me-2">{{ index + 1 }}</span>
<v-icon color="white">mdi-ticket</v-icon>
مرکز درآمد
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon color="white" @click="removeItem(index)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-toolbar>
<v-card-text>
<v-row>
<v-col cols="12" md="4">
<Htabletreeselect v-model="item.id" label="مرکز درآمد" tableType="income" />
</v-col>
<v-col cols="12" md="4">
<Hnumberinput
v-model="item.amount"
label="مبلغ"
variant="outlined"
density="compact"
@update:model-value="calc"
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="item.des"
label="شرح"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<!-- حساب بانکی -->
<template v-for="(item, index) in banks" :key="'bank'+index">
<v-card class="mb-4">
<v-toolbar color="grey-lighten-2" density="compact">
<v-toolbar-title>
<span class="me-2">{{ index + 1 }}</span>
<v-icon>mdi-bank</v-icon>
حساب بانکی حساب بانکی
</button> </v-toolbar-title>
<button @click="addCashdesk()" type="button" class="dropdown-item" href="javascript:void(0)"> <v-spacer></v-spacer>
<i class="fa fa-money-bill-wheat"></i> <v-btn icon color="error" @click="removeBank(index)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-toolbar>
<v-card-text>
<v-row>
<v-col cols="12" md="4">
<v-autocomplete
v-model="item.id"
:items="listBanks"
item-title="name"
item-value="id"
label="بانک"
variant="outlined"
density="compact"
></v-autocomplete>
</v-col>
<v-col cols="12" md="4">
<Hnumberinput
v-model="item.amount"
label="مبلغ"
variant="outlined"
density="compact"
@update:model-value="calc"
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="item.des"
label="شرح"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<!-- صندوق -->
<template v-for="(item, index) in cashdesks" :key="'cashdesk'+index">
<v-card class="mb-4">
<v-toolbar color="grey-lighten-3" density="compact">
<v-toolbar-title>
<span class="me-2">{{ index + 1 }}</span>
<v-icon>mdi-cash-register</v-icon>
صندوق صندوق
</button> </v-toolbar-title>
<button @click="addSalary()" type="button" class="dropdown-item" href="javascript:void(0)"> <v-spacer></v-spacer>
<i class="fa fa-dot-circle"></i> <v-btn icon color="error" @click="removeCashdesk(index)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-toolbar>
<v-card-text>
<v-row>
<v-col cols="12" md="4">
<v-autocomplete
v-model="item.id"
:items="listCashdesks"
item-title="name"
item-value="id"
label="صندوق"
variant="outlined"
density="compact"
></v-autocomplete>
</v-col>
<v-col cols="12" md="4">
<Hnumberinput
v-model="item.amount"
label="مبلغ"
variant="outlined"
density="compact"
@update:model-value="calc"
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="item.des"
label="شرح"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<!-- تنخواه گردان -->
<template v-for="(item, index) in salarys" :key="'salary'+index">
<v-card class="mb-4">
<v-toolbar color="grey-lighten-3" density="compact">
<v-toolbar-title>
<span class="me-2">{{ index + 1 }}</span>
<v-icon>mdi-wallet</v-icon>
تنخواه گردان تنخواه گردان
</button> </v-toolbar-title>
<button @click="addPerson()" type="button" class="dropdown-item" href="javascript:void(0)"> <v-spacer></v-spacer>
<i class="fa fa-person"></i> <v-btn icon color="error" @click="removeSalary(index)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-toolbar>
<v-card-text>
<v-row>
<v-col cols="12" md="4">
<v-autocomplete
v-model="item.id"
:items="listSalarys"
item-title="name"
item-value="id"
label="تنخواه گردان"
variant="outlined"
density="compact"
></v-autocomplete>
</v-col>
<v-col cols="12" md="4">
<Hnumberinput
v-model="item.amount"
label="مبلغ"
variant="outlined"
density="compact"
@update:model-value="calc"
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="item.des"
label="شرح"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<!-- شخص -->
<template v-for="(item, index) in persons" :key="'person'+index">
<v-card class="mb-4">
<v-toolbar color="grey-lighten-3" density="compact">
<v-toolbar-title>
<span class="me-2">{{ index + 1 }}</span>
<v-icon>mdi-account</v-icon>
شخص شخص
</button> </v-toolbar-title>
</div> <v-spacer></v-spacer>
</div> <v-btn icon color="error" @click="removePerson(index)">
</div> <v-icon>mdi-delete</v-icon>
</div> </v-btn>
</div> </v-toolbar>
</div>
<div class="container border border-danger rounded-2 my-3 p-3"> <v-card-text>
<div class="row"> <v-row>
<div class="row"> <v-col cols="12" md="4">
<div class="col-12 border-bottom border-danger"> <Hpersonsearch v-model="item.id" label="شخص" />
مجموع دریافتها: </v-col>
<span class="text-danger">{{ $filters.formatNumber(sum) }}</span> <v-col cols="12" md="4">
</div> <Hnumberinput
</div> v-model="item.amount"
<div class="row"> label="مبلغ"
<div class="col-12 border-top border-danger"> variant="outlined"
باقیمانده: density="compact"
<span class="text-danger">{{ $filters.formatNumber(balance) }}</span> @update:model-value="calc"
</div> />
</div> </v-col>
</div> <v-col cols="12" md="4">
</div> <v-text-field
</div> v-model="item.des"
</div> label="شرح"
</div> variant="outlined"
density="compact"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
</v-container>
<!-- لودینگ -->
<v-overlay v-model="isLoading" class="align-center justify-center">
<v-progress-circular indeterminate size="64"></v-progress-circular>
</v-overlay>
</v-container>
</template> </template>
<script> <script>
@ -365,19 +331,23 @@ import axios from "axios";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import Loading from 'vue-loading-overlay'; import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/css/index.css'; import 'vue-loading-overlay/dist/css/index.css';
import VuePersianDatetimePicker from 'vue-persian-datetime-picker'
import Treeselect from 'vue3-treeselect'
// import the styles
import 'vue3-treeselect/dist/vue3-treeselect.css'
import archiveUpload from "../component/archive/archiveUpload.vue"; import archiveUpload from "../component/archive/archiveUpload.vue";
import Hdatepicker from "../../../components/forms/Hdatepicker.vue";
import Hnumberinput from "../../../components/forms/Hnumberinput.vue";
import Htabletreeselect from '../../../components/forms/Htabletreeselect.vue'
import Hpersonsearch from '../../../components/forms/Hpersonsearch.vue'
import quickAdd from "../component/person/quickAdd.vue"; import quickAdd from "../component/person/quickAdd.vue";
export default { export default {
name: "mod", name: "mod",
components: { components: {
Loading, Loading,
Treeselect,
archiveUpload, archiveUpload,
quickAdd quickAdd,
Hdatepicker,
Hnumberinput,
Htabletreeselect,
Hpersonsearch,
}, },
data: () => { data: () => {
return { return {
@ -392,27 +362,10 @@ export default {
listSalarys: [], listSalarys: [],
persons: [], persons: [],
incomes: [], incomes: [],
listIncomes:[],
banks: [], banks: [],
salarys: [], salarys: [],
cashdesks: [], cashdesks: [],
year: '', year: '',
currencyConfig: {
masked: false,
prefix: '',
suffix: 'ریال',
thousands: ',',
decimal: '.',
precision: 0,
disableNegative: false,
disabled: false,
min: 0,
max: null,
allowBlank: false,
minimumNumberOfCharacters: 0,
shouldRound: true,
focusOnRight: true,
},
data: { data: {
date: '', date: '',
des: '', des: '',
@ -427,41 +380,37 @@ export default {
}, },
methods: { methods: {
calc() { calc() {
this.sum = 0; this.sum = 0;
this.incomes.forEach((item) => { this.incomes.forEach((item) => {
this.sum = parseInt(this.sum) + parseInt(item.amount); this.sum = parseInt(this.sum) + (parseInt(item.amount) || 0);
}); });
let side = 0; let side = 0;
this.banks.forEach((item) => { this.banks.forEach((item) => {
side = parseInt(side) + parseInt(item.amount); side = parseInt(side) + (parseInt(item.amount) || 0);
}); });
this.salarys.forEach((item) => { this.salarys.forEach((item) => {
side = parseInt(side) + parseInt(item.amount); side = parseInt(side) + (parseInt(item.amount) || 0);
}); });
this.cashdesks.forEach((item) => { this.cashdesks.forEach((item) => {
side = parseInt(side) + parseInt(item.amount); side = parseInt(side) + (parseInt(item.amount) || 0);
}); });
this.persons.forEach((item) => { this.persons.forEach((item) => {
side = parseInt(side) + parseInt(item.amount); side = parseInt(side) + (parseInt(item.amount) || 0);
}); });
this.balance = parseInt(this.sum) - parseInt(side); this.balance = parseInt(this.sum) - parseInt(side);
this.funcCanSubmit(); this.funcCanSubmit();
}, },
funcCanSubmit() { funcCanSubmit() {
//check form can submit if (parseInt(this.balance) == 0 && this.sum > 0) {
if (
parseInt(this.balance) == 0 && this.sum > 0
) {
this.canSubmit = true; this.canSubmit = true;
} } else {
else {
this.canSubmit = false; this.canSubmit = false;
} }
}, },
addItem() { addItem() {
this.incomes.push({ this.incomes.push({
id: this.incomes[1], id: null,
amount: '', amount: '',
des: '' des: ''
}); });
@ -471,7 +420,7 @@ export default {
}, },
addBank() { addBank() {
this.banks.push({ this.banks.push({
person: null, id: null,
amount: '', amount: '',
des: '' des: ''
}) })
@ -481,7 +430,7 @@ export default {
}, },
addCashdesk() { addCashdesk() {
this.cashdesks.push({ this.cashdesks.push({
person: '', id: null,
amount: '', amount: '',
des: '' des: ''
}) })
@ -491,7 +440,7 @@ export default {
}, },
addSalary() { addSalary() {
this.salarys.push({ this.salarys.push({
person: '', id: null,
amount: '', amount: '',
des: '' des: ''
}) })
@ -501,7 +450,7 @@ export default {
}, },
addPerson() { addPerson() {
this.persons.push({ this.persons.push({
person: '', id: null,
amount: '', amount: '',
des: '' des: ''
}) })
@ -525,36 +474,36 @@ export default {
} }
else if (item.type == 'bank') { else if (item.type == 'bank') {
this.banks.push({ this.banks.push({
id: item.bank, id: item.bank.id,
amount: item.bd, amount: item.bd,
des: item.des des: item.des
}); });
} }
else if (item.type == 'cashdesk') { else if (item.type == 'cashdesk') {
this.cashdesks.push({ this.cashdesks.push({
id: item.cashdesk, id: item.cashdesk.id,
amount: item.bd, amount: item.bd,
des: item.des des: item.des
}); });
} }
else if (item.type == 'salary') { else if (item.type == 'salary') {
this.salarys.push({ this.salarys.push({
id: item.salary, id: item.salary.id,
amount: item.bd, amount: item.bd,
des: item.des des: item.des
}); });
} }
else if (item.type == 'person') { else if (item.type == 'person') {
this.persons.push({ this.persons.push({
id: item.person, id: item.person.id,
amount: item.bd, amount: item.bd,
des: item.des des: item.des
}); });
} }
}) })
this.calc();
}); });
} } else {
else {
//new //new
this.addBank(); this.addBank();
this.addItem(); this.addItem();
@ -564,10 +513,6 @@ export default {
this.data.date = response.data.now; this.data.date = response.data.now;
}) })
} }
//get list of items
axios.post('/api/accounting/table/childs/income').then((response) => {
this.listIncomes = response.data;
});
//get list of banks //get list of banks
axios.post('/api/bank/list').then((response) => { axios.post('/api/bank/list').then((response) => {
@ -583,18 +528,6 @@ export default {
axios.post('/api/salary/list').then((response) => { axios.post('/api/salary/list').then((response) => {
this.listSalarys = response.data; this.listSalarys = response.data;
}); });
//get list of persons
axios.post('/api/person/list/search').then((response) => {
this.listPersons = response.data;
});
},
searchPerson(query, loading) {
loading(true);
axios.post('/api/person/list/search', { search: query }).then((response) => {
this.listPersons = response.data;
loading(false);
});
}, },
save() { save() {
if (this.incomes.length == 0) { if (this.incomes.length == 0) {
@ -603,7 +536,9 @@ export default {
icon: 'error', icon: 'error',
confirmButtonText: 'قبول' confirmButtonText: 'قبول'
}); });
return;
} }
let sideOK = true; let sideOK = true;
this.banks.forEach((item) => { this.banks.forEach((item) => {
if (item.id == null || item.id == '') { if (item.id == null || item.id == '') {
@ -614,23 +549,24 @@ export default {
if (item.id == null || item.id == '') { if (item.id == null || item.id == '') {
sideOK = false; sideOK = false;
} }
}) });
this.cashdesks.forEach((item) => { this.cashdesks.forEach((item) => {
if (item.id == null || item.id == '') { if (item.id == null || item.id == '') {
sideOK = false; sideOK = false;
} }
}) });
this.persons.forEach((item) => { this.persons.forEach((item) => {
if (item.id == null || item.id == '') { if (item.id == null || item.id == '') {
sideOK = false; sideOK = false;
} }
}) });
if (sideOK == false) { if (sideOK == false) {
Swal.fire({ Swal.fire({
text: 'یکی از طرف‌های حساب انتخاب نشده است.', text: 'یکی از طرف‌های حساب انتخاب نشده است.',
icon: 'error', icon: 'error',
confirmButtonText: 'قبول' confirmButtonText: 'قبول'
}); });
return;
} }
let personOK = true; let personOK = true;
@ -645,12 +581,13 @@ export default {
icon: 'error', icon: 'error',
confirmButtonText: 'قبول' confirmButtonText: 'قبول'
}); });
return;
} }
if (personOK && sideOK) {
//going to save in api //going to save in api
//save persons pattern
let rows = []; let rows = [];
if (this.data.des == '') this.data.des = 'درآمد‌ها'; if (this.data.des == '') this.data.des = 'درآمد‌ها';
this.incomes.forEach((item) => { this.incomes.forEach((item) => {
if (item.des == '') item.des = 'درآمد' if (item.des == '') item.des = 'درآمد'
rows.push({ rows.push({
@ -661,11 +598,12 @@ export default {
type: 'calc', type: 'calc',
table: item.id table: item.id
}); });
}) });
this.banks.forEach((item) => { this.banks.forEach((item) => {
if (item.des == '') item.des = 'درآمد' if (item.des == '') item.des = 'درآمد'
rows.push({ rows.push({
id: item.id.id, id: item.id,
bd: parseInt(item.amount), bd: parseInt(item.amount),
bs: 0, bs: 0,
des: item.des, des: item.des,
@ -677,7 +615,7 @@ export default {
this.salarys.forEach((item) => { this.salarys.forEach((item) => {
if (item.des == '') item.des = 'درآمد' if (item.des == '') item.des = 'درآمد'
rows.push({ rows.push({
id: item.id.id, id: item.id,
bs: 0, bs: 0,
bd: parseInt(item.amount), bd: parseInt(item.amount),
des: item.des, des: item.des,
@ -689,7 +627,7 @@ export default {
this.persons.forEach((item) => { this.persons.forEach((item) => {
if (item.des == '') item.des = 'درآمد' if (item.des == '') item.des = 'درآمد'
rows.push({ rows.push({
id: item.id.id, id: item.id,
bs: 0, bs: 0,
bd: parseInt(item.amount), bd: parseInt(item.amount),
des: item.des, des: item.des,
@ -701,7 +639,7 @@ export default {
this.cashdesks.forEach((item) => { this.cashdesks.forEach((item) => {
if (item.des == '') item.des = 'درآمد' if (item.des == '') item.des = 'درآمد'
rows.push({ rows.push({
id: item.id.id, id: item.id,
bs: 0, bs: 0,
bd: parseInt(item.amount), bd: parseInt(item.amount),
des: item.des, des: item.des,
@ -716,7 +654,6 @@ export default {
type: 'income', type: 'income',
des: this.data.des, des: this.data.des,
rows: rows rows: rows
}).then((response) => { }).then((response) => {
if (response.data.result == 1) { if (response.data.result == 1) {
Swal.fire({ Swal.fire({
@ -736,9 +673,7 @@ export default {
confirmButtonText: 'قبول' confirmButtonText: 'قبول'
}); });
} }
}) });
}
} }
} }
} }

View file

@ -94,8 +94,7 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-textarea v-model="settings.sell.noteString" label="یادداشت پایین فاکتور" rows="4" <CustomEditor title="یادداشت پایین فاکتور" v-model="settings.sell.noteString" />
placeholder="این نوشته در پایین فاکتور‌ها چاپ خواهد شد"></v-textarea>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-select v-model="settings.sell.paper" :items="paperOptions" label="سایز کاغذ و حالت چاپ"></v-select> <v-select v-model="settings.sell.paper" :items="paperOptions" label="سایز کاغذ و حالت چاپ"></v-select>
@ -130,8 +129,7 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-textarea v-model="settings.buy.noteString" label="یادداشت پایین فاکتور" rows="4" <CustomEditor title="یادداشت پایین فاکتور" v-model="settings.buy.noteString" />
placeholder="این نوشته در پایین فاکتور‌ها چاپ خواهد شد"></v-textarea>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-select v-model="settings.buy.paper" :items="paperOptions" label="سایز کاغذ و حالت چاپ"></v-select> <v-select v-model="settings.buy.paper" :items="paperOptions" label="سایز کاغذ و حالت چاپ"></v-select>
@ -166,8 +164,7 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-textarea v-model="settings.rfbuy.noteString" label="یادداشت پایین فاکتور" rows="4" <CustomEditor title="یادداشت پایین فاکتور" v-model="settings.rfbuy.noteString" />
placeholder="این نوشته در پایین فاکتور‌ها چاپ خواهد شد"></v-textarea>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-select v-model="settings.rfbuy.paper" :items="paperOptions" label="سایز کاغذ و حالت چاپ"></v-select> <v-select v-model="settings.rfbuy.paper" :items="paperOptions" label="سایز کاغذ و حالت چاپ"></v-select>
@ -202,8 +199,7 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-textarea v-model="settings.rfsell.noteString" label="یادداشت پایین فاکتور" rows="4" <CustomEditor title="یادداشت پایین فاکتور" v-model="settings.rfsell.noteString" />
placeholder="این نوشته در پایین فاکتور‌ها چاپ خواهد شد"></v-textarea>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-select v-model="settings.rfsell.paper" :items="paperOptions" label="سایز کاغذ و حالت چاپ"></v-select> <v-select v-model="settings.rfsell.paper" :items="paperOptions" label="سایز کاغذ و حالت چاپ"></v-select>