almost finish buy system with new changes in hesabdariDoc

This commit is contained in:
Hesabix 2025-08-19 23:50:52 +00:00
parent 45c03051a0
commit 9af86b989b
9 changed files with 952 additions and 199 deletions

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250819234842 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_doc CHANGE is_preview is_preview TINYINT(1) DEFAULT 0, CHANGE is_approved is_approved TINYINT(1) DEFAULT 1
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_doc CHANGE is_preview is_preview TINYINT(1) DEFAULT NULL, CHANGE is_approved is_approved TINYINT(1) DEFAULT NULL
SQL);
}
}

View file

@ -250,7 +250,7 @@ class ApprovalController extends AbstractController
foreach ($docIds as $docId) {
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'id' => $docId,
'code' => $docId,
'bid' => $business
]);
@ -378,6 +378,274 @@ class ApprovalController extends AbstractController
}
}
#[Route('/api/approval/approve/buy/{docId}', name: 'api_approval_approve_buy', methods: ['POST'])]
public function approveBuyInvoice(
$docId,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('buy');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'code' => $docId,
'bid' => $business
]);
if (!$document) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید یافت نشد']);
}
$canApprove = $this->canUserApproveBuyInvoice($user, $businessSettings);
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
}
$document->setIsPreview(false);
$document->setIsApproved(true);
$document->setApprovedBy($user);
$entityManager->persist($document);
$entityManager->flush();
$logService->insert(
'تأیید فاکتور خرید',
"فاکتور خرید {$document->getCode()} توسط {$user->getFullName()} تأیید شد",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'فاکتور خرید با موفقیت تأیید شد'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در تأیید فاکتور خرید: ' . $e->getMessage()
], 500);
}
}
#[Route('/api/approval/approve/group/buy', name: 'api_approval_approve_group_buy', methods: ['POST'])]
public function approveBuyInvoiceGroup(
Request $request,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('buy');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$canApprove = $this->canUserApproveBuyInvoice($user, $businessSettings);
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتورها را ندارید']);
}
$data = json_decode($request->getContent(), true);
$docIds = $data['docIds'] ?? [];
foreach ($docIds as $docId) {
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'code' => $docId,
'bid' => $business
]);
if (!$document) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید یافت نشد']);
}
if ($document->isApproved()) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید تایید شده است']);
}
$document->setIsPreview(false);
$document->setIsApproved(true);
$document->setApprovedBy($user);
$entityManager->persist($document);
}
$entityManager->flush();
$logService->insert(
'تأیید فاکتورهای خرید',
"فاکتورهای خرید " . implode(', ', $docIds) . " توسط {$user->getFullName()} تأیید شدند",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'فاکتورهای خرید با موفقیت تأیید شدند'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در تأیید فاکتورهای خرید: ' . $e->getMessage()
], 500);
}
}
#[Route('/api/approval/unapprove/buy/{docId}', name: 'api_approval_unapprove_buy', methods: ['POST'])]
public function unapproveBuyInvoice(
$docId,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('buy');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'code' => $docId,
'bid' => $business
]);
if (!$document) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید یافت نشد']);
}
$canApprove = $this->canUserApproveBuyInvoice($user, $businessSettings);
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
}
$document->setIsPreview(true);
$document->setIsApproved(false);
$document->setApprovedBy(null);
$entityManager->persist($document);
$entityManager->flush();
$logService->insert(
'لغو تأیید فاکتور خرید',
"فاکتور خرید {$document->getCode()} توسط {$user->getFullName()} لغو تأیید شد",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'فاکتور خرید با موفقیت لغو تأیید شد'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در لغو تأیید فاکتور خرید: ' . $e->getMessage()
], 500);
}
}
#[Route('/api/approval/unapprove/group/buy', name: 'api_approval_unapprove_group_buy', methods: ['POST'])]
public function unapproveBuyInvoiceGroup(
Request $request,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('buy');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$canApprove = $this->canUserApproveBuyInvoice($user, $businessSettings);
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتورها را ندارید']);
}
$data = json_decode($request->getContent(), true);
$docIds = $data['docIds'] ?? [];
foreach ($docIds as $docId) {
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'code' => $docId,
'bid' => $business
]);
if (!$document) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید یافت نشد']);
}
if (!$document->isApproved()) {
return $this->json(['success' => false, 'message' => 'فاکتور خرید تایید نشده است']);
}
$document->setIsPreview(true);
$document->setIsApproved(false);
$document->setApprovedBy(null);
$entityManager->persist($document);
}
$entityManager->flush();
$logService->insert(
'لغو تأیید فاکتورهای خرید',
"فاکتورهای خرید " . implode(', ', $docIds) . " توسط {$user->getFullName()} لغو تأیید شدند",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'فاکتورهای خرید با موفقیت لغو تأیید شدند'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در لغو تأیید فاکتورهای خرید: ' . $e->getMessage()
], 500);
}
}
private function canUserApproveDocument(User $user, Business $business, HesabdariDoc $document): bool
{
if ($user->getEmail() === $business->getOwner()->getEmail()) {
@ -432,4 +700,13 @@ class ApprovalController extends AbstractController
return $business->getApproverSellInvoice() === $user->getEmail();
}
private function canUserApproveBuyInvoice(User $user, Business $business): bool
{
if ($user->getEmail() === $business->getOwner()->getEmail()) {
return true;
}
return $business->getApproverBuyInvoice() === $user->getEmail();
}
}

View file

@ -252,6 +252,9 @@ class BuyController extends AbstractController
return $this->json($extractor->notFound());
}
foreach ($params['items'] as $item) {
if (!$item || !isset($item['code'])) {
continue;
}
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
@ -286,77 +289,177 @@ class BuyController extends AbstractController
return $this->json($extractor->operationSuccess());
}
#[Route('/api/buy/docs/search', name: 'app_buy_docs_search')]
#[Route('/api/buy/docs/search', name: 'app_buy_docs_search', methods: ['POST'])]
public function app_buy_docs_search(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('buy');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
$params = json_decode($request->getContent(), true) ?? [];
$searchTerm = $params['search'] ?? '';
$page = max(1, $params['page'] ?? 1);
$perPage = max(1, min(100, $params['perPage'] ?? 10));
$types = $params['types'] ?? [];
$sortBy = $params['sortBy'] ?? [];
$queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('d.isPreview, d.isApproved')
->addSelect('u.fullName as submitter')
->addSelect('approver.fullName as approvedByName, approver.id as approvedById, approver.email as approvedByEmail')
->addSelect('l.code as labelCode, l.label as labelLabel')
->from(HesabdariDoc::class, 'd')
->leftJoin('d.submitter', 'u')
->leftJoin('d.approvedBy', 'approver')
->leftJoin('d.InvoiceLabel', 'l')
->leftJoin('d.hesabdariRows', 'r')
->where('d.bid = :bid')
->andWhere('d.year = :year')
->andWhere('d.type = :type')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('type', 'buy')
->setParameter('money', $acc['money']);
if ($searchTerm) {
$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',
'p.mobile LIKE :search'
)
)
->setParameter('search', "%$searchTerm%");
}
$data = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'type' => 'buy',
'money' => $acc['money']
], [
'id' => 'DESC'
]);
if (!empty($types)) {
$queryBuilder->andWhere('l.code IN (:types)')
->setParameter('types', $types);
}
// فیلدهای معتبر برای مرتب‌سازی توی دیتابیس
$validDbFields = [
'id' => 'd.id',
'dateSubmit' => 'd.dateSubmit',
'date' => 'd.date',
'type' => 'd.type',
'code' => 'd.code',
'des' => 'd.des',
'amount' => 'd.amount',
'mdate' => 'd.mdate',
'plugin' => 'd.plugin',
'refData' => 'd.refData',
'shortlink' => 'd.shortlink',
'isPreview' => 'd.isPreview',
'isApproved' => 'd.isApproved',
'approvedBy' => 'd.approvedBy',
'submitter' => 'u.fullName',
'label' => 'l.label',
];
// اعمال مرتب‌سازی توی دیتابیس
if (!empty($sortBy)) {
foreach ($sortBy as $sort) {
$key = $sort['key'] ?? 'id';
$direction = isset($sort['order']) && strtoupper($sort['order']) === 'DESC' ? 'DESC' : 'ASC';
if (isset($validDbFields[$key])) {
$queryBuilder->addOrderBy($validDbFields[$key], $direction);
}
}
} else {
$queryBuilder->orderBy('d.id', 'DESC');
}
$totalItemsQuery = clone $queryBuilder;
$totalItems = $totalItemsQuery->select('COUNT(DISTINCT d.id)')
->getQuery()
->getSingleScalarResult();
$queryBuilder->setFirstResult(($page - 1) * $perPage)
->setMaxResults($perPage);
$docs = $queryBuilder->getQuery()->getArrayResult();
$dataTemp = [];
foreach ($data as $item) {
$temp = [
'id' => $item->getId(),
'dateSubmit' => $item->getDateSubmit(),
'date' => $item->getDate(),
'type' => $item->getType(),
'code' => $item->getCode(),
'des' => $item->getDes(),
'amount' => $item->getAmount(),
'submitter' => $item->getSubmitter()->getFullName(),
foreach ($docs as $doc) {
$item = [
'id' => $doc['id'],
'dateSubmit' => $doc['dateSubmit'],
'date' => $doc['date'],
'type' => $doc['type'],
'code' => $doc['code'],
'des' => $doc['des'],
'amount' => $doc['amount'],
'submitter' => $doc['submitter'],
'label' => $doc['labelCode'] ? [
'code' => $doc['labelCode'],
'label' => $doc['labelLabel']
] : null,
'isPreview' => $doc['isPreview'],
'isApproved' => $doc['isApproved'],
'approvedBy' => $doc['approvedByName'] ? [
'fullName' => $doc['approvedByName'],
'id' => $doc['approvedById'],
'email' => $doc['approvedByEmail']
] : null,
];
$mainRow = $entityManager->getRepository(HesabdariRow::class)->getNotEqual($item, 'person');
$temp['person'] = '';
if ($mainRow)
$temp['person'] = Explore::ExplorePerson($mainRow->getPerson());
$temp['label'] = null;
if ($item->getInvoiceLabel()) {
$temp['label'] = [
'code' => $item->getInvoiceLabel()->getCode(),
'label' => $item->getInvoiceLabel()->getLabel()
];
$mainRow = $entityManager->getRepository(HesabdariRow::class)
->createQueryBuilder('r')
->where('r.doc = :docId')
->andWhere('r.person IS NOT NULL')
->setParameter('docId', $doc['id'])
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
$item['person'] = $mainRow && $mainRow->getPerson() ? Explore::ExplorePerson($mainRow->getPerson()) : null;
// محاسبه پرداختی‌ها
$relatedDocs = $entityManager->getRepository(HesabdariDoc::class)
->createQueryBuilder('rd')
->select('SUM(rd.amount) as total_pays, COUNT(rd.id) as count_docs')
->innerJoin('rd.relatedDocs', 'rel')
->where('rel.id = :sourceDocId')
->andWhere('rd.bid = :bidId')
->setParameter('sourceDocId', $doc['id'])
->setParameter('bidId', $acc['bid']->getId())
->getQuery()
->getSingleResult();
$item['relatedDocsCount'] = (int) $relatedDocs['count_docs'];
$item['relatedDocsPays'] = $relatedDocs['total_pays'] ?? 0;
// محاسبه کالاها و تخفیف/هزینه حمل
$item['commodities'] = [];
$item['discountAll'] = 0;
$item['transferCost'] = 0;
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy(['doc' => $doc['id']]);
foreach ($rows as $row) {
if ($row->getRef()->getCode() == '51') {
$item['discountAll'] = $row->getBs();
} elseif ($row->getRef()->getCode() == '90') {
$item['transferCost'] = $row->getBd();
} elseif ($row->getCommodity()) {
$item['commodities'][] = Explore::ExploreCommodity($row->getCommodity(), $row->getCommdityCount());
}
}
$temp['relatedDocsCount'] = count($item->getRelatedDocs());
$pays = 0;
foreach ($item->getRelatedDocs() as $relatedDoc) {
$pays += $relatedDoc->getAmount();
$dataTemp[] = $item;
}
$temp['relatedDocsPays'] = $pays;
$temp['commodities'] = [];
foreach ($item->getHesabdariRows() as $item) {
if ($item->getRef()->getCode() == '51') {
$temp['discountAll'] = $item->getBs();
} elseif ($item->getRef()->getCode() == '90') {
$temp['transferCost'] = $item->getBd();
}
if ($item->getCommodity()) {
$temp['commodities'][] = Explore::ExploreCommodity($item->getCommodity(), $item->getCommdityCount());
}
}
if (!array_key_exists('discountAll', $temp))
$temp['discountAll'] = 0;
if (!array_key_exists('transferCost', $temp))
$temp['transferCost'] = 0;
$dataTemp[] = $temp;
}
return $this->json($dataTemp);
return $this->json([
'items' => $dataTemp,
'total' => (int) $totalItems,
'page' => $page,
'perPage' => $perPage,
]);
}
#[Route('/api/buy/posprinter/invoice', name: 'app_buy_posprinter_invoice')]
@ -425,6 +528,8 @@ class BuyController extends AbstractController
return $this->json(['id' => $pdfPid]);
}
#[Route('/api/buy/print/invoice', name: 'app_buy_print_invoice')]
public function app_buy_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, TemplateRenderer $renderer): JsonResponse
{
@ -586,4 +691,43 @@ class BuyController extends AbstractController
}
return $this->json(['id' => $pdfPid]);
}
#[Route('/api/buy/approve/{code}', name: 'app_buy_approve', methods: ['POST'])]
public function approveBuyDoc(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('buy');
if (!$acc) throw $this->createAccessDeniedException();
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
'money' => $acc['money']
]);
if (!$doc) throw $this->createNotFoundException('فاکتور یافت نشد');
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
$entityManager->persist($doc);
$entityManager->flush();
return $this->json(['result' => 0]);
}
#[Route('/api/buy/payment/approve/{code}', name: 'app_buy_payment_approve', methods: ['POST'])]
public function approveBuyPayment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('buy');
if (!$acc) throw $this->createAccessDeniedException();
$paymentDoc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code,
'money' => $acc['money'],
'type' => 'buy_pay'
]);
if (!$paymentDoc) throw $this->createNotFoundException('سند پرداخت یافت نشد');
$paymentDoc->setIsPreview(false);
$paymentDoc->setIsApproved(true);
$paymentDoc->setApprovedBy($this->getUser());
$entityManager->persist($paymentDoc);
$entityManager->flush();
return $this->json(['result' => 0]);
}
}

View file

@ -728,11 +728,11 @@ class HesabdariDoc
}
// Approval fields
#[ORM\Column(nullable: true)]
private ?bool $isPreview = null;
#[ORM\Column(nullable: true, options: ['default' => false])]
private ?bool $isPreview = false;
#[ORM\Column(nullable: true)]
private ?bool $isApproved = null;
#[ORM\Column(nullable: true, options: ['default' => true])]
private ?bool $isApproved = true;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true)]

View file

@ -109,8 +109,12 @@ class HesabdariDocRepository extends ServiceEntityRepository
$qb = $this->createQueryBuilder('h');
foreach ($criteria as $field => $value) {
if ($field === 'bid' && is_object($value)) {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value->getId());
} else {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value);
}
}
$qb->andWhere('(h.isApproved = 1 OR (h.isApproved = 0 AND h.isPreview = 0))');
@ -129,8 +133,12 @@ class HesabdariDocRepository extends ServiceEntityRepository
$qb = $this->createQueryBuilder('h');
foreach ($criteria as $field => $value) {
if ($field === 'bid' && is_object($value)) {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value->getId());
} else {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value);
}
}
if ($orderBy) {
foreach ($orderBy as $field => $direction) {
@ -146,8 +154,12 @@ class HesabdariDocRepository extends ServiceEntityRepository
$qb = $this->createQueryBuilder('h');
foreach ($criteria as $field => $value) {
if ($field === 'bid' && is_object($value)) {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value->getId());
} else {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value);
}
}
$qb->andWhere('(h.isApproved = 1 OR (h.isApproved = 0 AND h.isPreview = 0))');
@ -175,4 +187,34 @@ class HesabdariDocRepository extends ServiceEntityRepository
->getQuery()
->getResult();
}
//include preview
public function findByIncludePreview(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
$qb = $this->createQueryBuilder('h');
foreach ($criteria as $field => $value) {
if ($field === 'bid' && is_object($value)) {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value->getId());
} else {
$qb->andWhere("h.$field = :$field")->setParameter($field, $value);
}
}
if ($orderBy) {
foreach ($orderBy as $field => $direction) {
$qb->addOrderBy("h.$field", $direction);
}
}
if ($limit) {
$qb->setMaxResults($limit);
}
if ($offset) {
$qb->setFirstResult($offset);
}
return $qb->getQuery()->getResult();
}
}

View file

@ -28,6 +28,17 @@
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary" v-if="checkApprover()">عملیات گروهی تایید</v-list-subheader>
<v-list-item v-if="currentTab === 'pending' && checkApprover()" class="text-dark" title="تایید فاکتورهای انتخابی" @click="approveSelectedInvoices">
<template v-slot:prepend>
<v-icon color="success" icon="mdi-check-decagram"></v-icon>
</template>
</v-list-item>
<v-list-item v-if="currentTab === 'approved' && checkApprover()" class="text-dark" title="لغو تایید فاکتورهای انتخابی" @click="unapproveSelectedInvoices">
<template v-slot:prepend>
<v-icon color="red" icon="mdi-cancel"></v-icon>
</template>
</v-list-item>
<v-list-subheader color="primary">تغییر برچسبها</v-list-subheader>
<v-list-item v-for="item in types" class="text-dark" :title="'تغییر به ' + item.label" @click="changeLabel(item)">
<template v-slot:prepend>
@ -43,6 +54,14 @@
</v-menu>
</v-toolbar>
<!-- Tabs for two-step approval -->
<div v-if="business.requireTwoStepApproval" class="px-2 pt-2">
<v-tabs v-model="currentTab" color="primary" density="comfortable" grow>
<v-tab value="approved">فاکتورهای تایید شده</v-tab>
<v-tab value="pending">فاکتورهای در انتظار تایید</v-tab>
</v-tabs>
</div>
<!-- فیلد جستجو و فیلتر -->
<v-text-field
hide-details
@ -88,48 +107,50 @@
</v-text-field>
<!-- جدول اصلی -->
<EasyDataTable
v-model:items-selected="itemsSelected"
table-class-name="customize-table"
show-index
alternating
:headers="headers"
:items="items"
theme-color="#1d90ff"
header-text-direction="center"
body-text-direction="center"
rowsPerPageMessage="تعداد سطر"
emptyMessage="اطلاعاتی برای نمایش وجود ندارد"
rowsOfPageSeparatorMessage="از"
:loading="loading"
>
<template #item-operation="{ code }">
<v-data-table-server v-model:items-per-page="serverOptions.rowsPerPage" v-model:page="serverOptions.page"
:headers="visibleHeaders" :items="displayItems" :items-length="displayTotal" :loading="loading"
:no-data-text="$t('table.no_data')" v-model="itemsSelected" show-select class="elevation-1 data-table-wrapper" item-value="code"
:max-height="tableHeight" :header-props="{ class: 'custom-header' }" @update:options="updateServerOptions"
multi-sort>
<template v-slot:item.operation="{ item }">
<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/accounting/view/' + code" class="text-dark" title="سند حسابداری">
<v-list-item :to="'/acc/accounting/view/' + item.code" class="text-dark" title="سند حسابداری">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-file"></v-icon>
</template>
</v-list-item>
<v-list-item :to="'/acc/buy/view/' + code" class="text-dark" title="مشاهده">
<v-list-item :to="'/acc/buy/view/' + item.code" class="text-dark" title="مشاهده">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-eye"></v-icon>
</template>
</v-list-item>
<v-list-item @click="openPrintModal(code)" class="text-dark" title="خروجی PDF">
<v-list-item @click="openPrintModal(item.code)" class="text-dark" title="خروجی PDF">
<template v-slot:prepend>
<v-icon icon="mdi-file-pdf-box"></v-icon>
</template>
</v-list-item>
<v-list-item @click="canEditItem(code)" class="text-dark" title="ویرایش">
<v-list-item v-if="canShowApprovalButton(item)" class="text-dark" title="تایید فاکتور"
@click="approveInvoice(item.code)">
<template v-slot:prepend>
<v-icon color="success">mdi-check-decagram</v-icon>
</template>
</v-list-item>
<v-list-item v-if="canShowUnapproveButton(item)" class="text-dark" title="لغو تایید فاکتور"
@click="unapproveInvoice(item.code)">
<template v-slot:prepend>
<v-icon color="red">mdi-cancel</v-icon>
</template>
</v-list-item>
<v-list-item @click="canEditItem(item.code)" class="text-dark" title="ویرایش">
<template v-slot:prepend>
<v-icon icon="mdi-file-edit"></v-icon>
</template>
</v-list-item>
<v-list-item @click="deleteItem(code)" class="text-dark" title="حذف">
<v-list-item @click="deleteItem(item.code)" class="text-dark" title="حذف">
<template v-slot:prepend>
<v-icon color="deep-orange-accent-4" icon="mdi-trash-can"></v-icon>
</template>
@ -137,48 +158,56 @@
</v-list>
</v-menu>
</template>
<template #item-label="{ label }">
<span v-if="label">
<span v-if="label.code == 'payed'" class="text-success">{{ label.label }}</span>
<span v-if="label.code == 'returned'" class="text-danger">{{ label.label }}</span>
<span v-if="label.code == 'accepted'" class="text-info">{{ label.label }}</span>
<template v-slot:item.label="{ item }">
<span v-if="item.label">
<span v-if="item.label.code == 'payed'" class="text-success">{{ item.label.label }}</span>
<span v-if="item.label.code == 'returned'" class="text-danger">{{ item.label.label }}</span>
<span v-if="item.label.code == 'accepted'" class="text-info">{{ item.label.label }}</span>
</span>
</template>
<template #item-des="{ des }">
{{ des.replace("فاکتور خرید:", "") }}
<template v-slot:item.des="{ item }">
{{ item.des.replace("فاکتور خرید:", "") }}
</template>
<template #item-relatedDocsCount="{ relatedDocsCount, relatedDocsPays }">
<span v-if="relatedDocsCount != '0'" class="text-success">
<v-icon small>mdi-currency-usd</v-icon>
{{ $filters.formatNumber(relatedDocsPays) }}
<template v-slot:item.relatedDocsCount="{ item }">
<span v-if="item.relatedDocsCount != '0'" class="text-success">
{{ $filters.formatNumber(item.relatedDocsPays) }}
</span>
</template>
<template #item-amount="{ amount }">
<template v-slot:item.amount="{ item }">
<span class="text-dark">
{{ $filters.formatNumber(amount) }}
{{ $filters.formatNumber(item.amount) }}
</span>
</template>
<template #item-transferCost="{ transferCost }">
<template v-slot:item.transferCost="{ item }">
<span class="text-dark">
{{ $filters.formatNumber(transferCost) }}
{{ $filters.formatNumber(item.transferCost) }}
</span>
</template>
<template #item-discountAll="{ discountAll }">
<template v-slot:item.discountAll="{ item }">
<span class="text-dark">
{{ $filters.formatNumber(discountAll) }}
{{ $filters.formatNumber(item.discountAll) }}
</span>
</template>
<template #item-person="{ person }">
<router-link :to="'/acc/persons/card/view/' + person.code">
{{ person.nikename }}
<template v-slot:item.person="{ item }">
<router-link v-if="item.person" :to="'/acc/persons/card/view/' + item.person.code">
{{ item.person.nikename }}
</router-link>
<span v-else>-</span>
</template>
<template v-slot:item.approvalStatus="{ item }">
<v-chip size="small" :color="getApprovalStatusColor(item)">
{{ getApprovalStatusText(item) }}
</v-chip>
</template>
<template v-slot:item.approvedBy="{ item }">
{{ item.approvedBy?.fullName || '-' }}
</template>
<template v-slot:item.code="{ item }">
<router-link :to="'/acc/buy/view/' + item.code">
{{ item.code }}
</router-link>
</template>
<template #item-code="{ code }">
<router-link :to="'/acc/buy/view/' + code">
{{ code }}
</router-link>
</template>
</EasyDataTable>
</v-data-table-server>
<!-- مودال چاپ -->
<v-dialog v-model="printModal" width="auto">
@ -236,11 +265,14 @@
<script>
import axios from "axios";
import Swal from "sweetalert2";
import { ref } from "vue";
import { defineComponent, reactive, watch } from "vue";
import debounce from "lodash/debounce";
export default {
export default defineComponent({
name: "list",
data: () => ({
data() {
return {
currentTab: 'approved',
printModal: false,
printOptions: {
pays: true,
@ -257,27 +289,63 @@ export default {
{ title: 'A5 عمودی', value: 'A5' },
{ title: 'A5 افقی', value: 'A5-L' },
],
business: { requireTwoStepApproval: false, approvers: { buyInvoice: null } },
currentUser: { email: '', owner: false },
sumSelected: 0,
sumTotal: 0,
itemsSelected: [],
searchValue: '',
types: [],
loading: ref(true),
loading: false,
items: [],
orgItems: [],
headers: [
{ text: "عملیات", value: "operation" },
{ text: "فاکتور", value: "code", sortable: true },
{ text: "تاریخ", value: "date", sortable: true },
{ text: "خریدار", value: "person", sortable: true },
{ text: "تخفیف", value: "discountAll", sortable: true },
{ text: "حمل و نقل", value: "transferCost", sortable: true },
{ text: "مبلغ", value: "amount", sortable: true },
{ text: "پرداختی", value: "relatedDocsCount", sortable: true },
{ text: "برچسب", value: "label", width: 100 },
{ text: "شرح", value: "des", sortable: true },
]
itemsApproved: [],
itemsPending: [],
total: 0,
totalApproved: 0,
totalPending: 0,
serverOptions: reactive({
page: 1,
rowsPerPage: 10,
sortBy: [],
}),
allHeaders: [
{ title: "عملیات", value: "operation", sortable: false, visible: true, width: 100 },
{ title: "فاکتور", value: "code", sortable: true, visible: true, width: 120 },
{ title: "تاریخ", value: "date", sortable: true, visible: true, width: 120 },
{ title: "خریدار", value: "person", sortable: true, visible: true, width: 150 },
{ title: "وضعیت تایید", value: "approvalStatus", sortable: true, visible: true, width: 150 },
{ title: "تاییدکننده", value: "approvedBy", sortable: true, visible: true, width: 120 },
{ title: "تخفیف", value: "discountAll", sortable: true, visible: true, width: 120 },
{ title: "حمل و نقل", value: "transferCost", sortable: true, visible: true, width: 120 },
{ title: "مبلغ", value: "amount", sortable: true, visible: true, width: 150 },
{ title: "پرداختی", value: "relatedDocsCount", sortable: true, visible: true, width: 150 },
{ title: "برچسب", value: "label", sortable: true, visible: true, width: 120 },
{ title: "شرح", value: "des", sortable: true, visible: true, minWidth: 200 },
],
tableHeight: 500,
};
},
computed: {
visibleHeaders() {
return this.allHeaders.filter(header => {
if ((header.value === 'approvalStatus' || header.value === 'approvedBy') && !this.business.requireTwoStepApproval) {
return false;
}
return header.visible;
});
},
tableHeight() {
return window.innerHeight - 200;
},
displayItems() {
if (!this.business.requireTwoStepApproval) return this.items;
return this.currentTab === 'pending' ? this.itemsPending : this.itemsApproved;
},
displayTotal() {
if (!this.business.requireTwoStepApproval) return this.total;
return this.currentTab === 'pending' ? this.totalPending : this.totalApproved;
},
},
methods: {
openPrintModal(code) {
this.printOptions.selectedPrintCode = code;
@ -293,7 +361,7 @@ export default {
} else {
this.loading = true;
axios.post('/api/buy/label/change', {
'items': this.itemsSelected,
'items': this.itemsSelected.filter(item => item && typeof item === 'string').map(item => ({ code: item })),
'label': label
}).then((response) => {
this.loading = false;
@ -315,6 +383,130 @@ export default {
});
}
},
checkApprover() {
return this.business.requireTwoStepApproval && (this.business.approvers.buyInvoice == this.currentUser.email || this.currentUser.owner === true);
},
async loadBusinessInfo() {
try {
const response = await axios.get('/api/business/get/info/' + localStorage.getItem('activeBid'));
this.business = response.data || { requireTwoStepApproval: false, approvers: { buyInvoice: null } };
} catch (error) {
console.error('Error loading business info:', error);
this.business = { requireTwoStepApproval: false, approvers: { buyInvoice: null } };
}
},
async loadCurrentUser() {
try {
const response = await axios.post('/api/business/get/user/permissions');
this.currentUser = response.data || { email: '', owner: false };
} catch (error) {
console.error('Error loading current user:', error);
this.currentUser = { email: '', owner: false };
}
},
getApprovalStatusText(item) {
if (!item || !this.business?.requireTwoStepApproval) return 'تایید دو مرحله‌ای غیرفعال';
if (item.isPreview) return 'در انتظار تایید';
if (item.isApproved) return 'تایید شده';
return 'تایید شده';
},
getApprovalStatusColor(item) {
if (!item || !this.business?.requireTwoStepApproval) return 'default';
if (item.isPreview) return 'warning';
if (item.isApproved) return 'success';
return 'success';
},
canShowApprovalButton(item) {
if (!this.checkApprover()) return false;
if (item?.isApproved) return false;
return true;
},
async approveInvoice(code) {
try {
this.loading = true;
await axios.post(`/api/approval/approve/buy/${code}`);
await this.loadData();
Swal.fire({ text: 'فاکتور تایید شد', icon: 'success', confirmButtonText: 'قبول' });
} catch (error) {
Swal.fire({ text: 'خطا در تایید فاکتور: ' + (error.response?.data?.message || error.message), icon: 'error', confirmButtonText: 'قبول' });
} finally {
this.loading = false;
}
},
canShowUnapproveButton(item) {
return !this.canShowApprovalButton(item) && this.checkApprover();
},
async unapproveInvoice(code) {
try {
this.loading = true;
await axios.post(`/api/approval/unapprove/buy/${code}`);
await this.loadData();
Swal.fire({ text: 'تایید فاکتور لغو شد', icon: 'success', confirmButtonText: 'قبول' });
} catch (error) {
Swal.fire({ text: 'خطا در لغو تایید فاکتور: ' + (error.response?.data?.message || error.message), icon: 'error', confirmButtonText: 'قبول' });
} finally {
this.loading = false;
}
},
getItemByCode(code) {
return this.items.find(item => item.code === code);
},
approveSelectedInvoices() {
if (this.itemsSelected.length === 0) {
Swal.fire({ text: 'هیچ موردی انتخاب نشده است.', icon: 'warning', confirmButtonText: 'قبول' });
return;
}
const selectedInvoices = this.items.filter(inv => this.itemsSelected.includes(inv.code));
if (selectedInvoices.some(inv => !(!inv.isApproved && inv.isPreview))) {
Swal.fire({ text: 'برخی فاکتور های انتخابی تایید شده هستند.', icon: 'warning', confirmButtonText: 'قبول' });
return;
}
Swal.fire({ title: 'تایید فاکتورهای انتخابی', text: 'فاکتورهای انتخاب‌شده تایید خواهند شد.', icon: 'question', showCancelButton: true, confirmButtonText: 'بله', cancelButtonText: 'خیر' })
.then(async (r) => {
if (!r.isConfirmed) return;
this.loading = true;
try {
await axios.post(`/api/approval/approve/group/buy`, {
'docIds': this.itemsSelected.filter(item => item && typeof item === 'string')
});
Swal.fire({ text: 'فاکتورها تایید شدند.', icon: 'success', confirmButtonText: 'قبول' });
this.itemsSelected = [];
this.loadData();
} catch (e) {
Swal.fire({ text: 'خطا در تایید فاکتورها', icon: 'error', confirmButtonText: 'قبول' });
} finally {
this.loading = false;
}
});
},
unapproveSelectedInvoices() {
if (this.itemsSelected.length === 0) {
Swal.fire({ text: 'هیچ موردی انتخاب نشده است.', icon: 'warning', confirmButtonText: 'قبول' });
return;
}
const selectedInvoices = this.items.filter(inv => this.itemsSelected.includes(inv.code));
if (selectedInvoices.some(inv => !(inv.isApproved && !inv.isPreview))) {
Swal.fire({ text: 'برخی فاکتور های انتخابی تایید نشده هستند.', icon: 'warning', confirmButtonText: 'قبول' });
return;
}
Swal.fire({ title: 'لغو تایید فاکتورهای انتخابی', text: 'فاکتورهای انتخاب‌شده لغو تایید خواهند شد.', icon: 'question', showCancelButton: true, confirmButtonText: 'بله', cancelButtonText: 'خیر' })
.then(async (r) => {
if (!r.isConfirmed) return;
this.loading = true;
try {
await axios.post(`/api/approval/unapprove/group/buy`, {
'docIds': this.itemsSelected.filter(item => item && typeof item === 'string')
});
Swal.fire({ text: 'تایید فاکتورها لغو شد.', icon: 'success', confirmButtonText: 'قبول' });
this.itemsSelected = [];
this.loadData();
} catch (e) {
Swal.fire({ text: 'خطا در لغو تایید فاکتورها', icon: 'error', confirmButtonText: 'قبول' });
} finally {
this.loading = false;
}
});
},
filterTable() {
this.loading = true;
let calcItems = [];
@ -350,27 +542,70 @@ export default {
}
this.loading = false;
},
loadData() {
axios.post("/api/printers/options/info").then((response) => {
this.printOptions = response.data.buy;
updateServerOptions(options) {
this.serverOptions.page = options.page;
this.serverOptions.rowsPerPage = options.itemsPerPage;
this.serverOptions.sortBy = options.sortBy || [];
this.loadData();
},
debouncedLoadData: debounce(function () {
this.loadData();
}, 300),
async loadData() {
this.loading = true;
try {
if (!this.printOptions.selectedPrintCode) {
const printResponse = await axios.post("/api/printers/options/info");
this.printOptions = printResponse.data.buy || this.printOptions;
}
const typesResponse = await axios.post('/api/invoice/types', { type: 'buy' });
this.types = typesResponse.data.map(t => ({
...t,
checked: this.types.find(x => x.code === t.code)?.checked ?? false
}));
const response = await axios.post('/api/buy/docs/search', {
type: 'buy',
search: this.searchValue,
page: this.serverOptions.page,
perPage: this.serverOptions.rowsPerPage,
types: this.types.filter(t => t.checked).map(t => t.code),
sortBy: this.serverOptions.sortBy,
});
axios.post('/api/invoice/types', {
type: 'buy'
}).then((response) => {
this.types = response.data;
});
const all = (response.data.items || []).map(item => ({
...item,
approvalStatus: this.business.requireTwoStepApproval ?
(item.isPreview ? 'pending' : 'approved') : 'disabled',
approvedBy: item.approvedBy || null
})).filter(item => item.code && typeof item.code !== 'undefined');
axios.post('/api/buy/docs/search', {
type: 'buy'
}).then((response) => {
this.items = response.data;
this.orgItems = response.data;
this.items.forEach((item) => {
this.sumTotal += parseInt(item.amount);
this.items = all;
this.total = Number(response.data.total) || 0;
if (this.business.requireTwoStepApproval) {
this.itemsApproved = all.filter(i => i.isApproved === true);
this.itemsPending = all.filter(i => i.isPreview === true && i.isApproved !== true);
this.totalApproved = this.itemsApproved.length;
this.totalPending = this.itemsPending.length;
}
this.sumTotal = this.displayItems.reduce((sum, item) => sum + parseInt(item.amount || 0), 0);
} catch (error) {
console.error('Error loading data:', error);
this.items = [];
this.total = 0;
this.sumTotal = 0;
Swal.fire({
text: 'خطا در بارگذاری داده‌ها: ' + error.message,
icon: 'error',
confirmButtonText: 'قبول'
});
} finally {
this.loading = false;
});
}
},
canEditItem(code) {
this.loading = true;
@ -405,7 +640,7 @@ export default {
if (result.isConfirmed) {
this.loading = true;
axios.post('/api/accounting/remove/group', {
'items': this.itemsSelected
'items': this.itemsSelected.filter(item => item && typeof item === 'string')
}).then((response) => {
this.loading = false;
if (response.data.result == 1) {
@ -474,55 +709,53 @@ export default {
});
}
});
},
updateServerOptions(options) {
this.serverOptions.page = options.page;
this.serverOptions.rowsPerPage = options.itemsPerPage;
this.serverOptions.sortBy = options.sortBy || [];
this.loadData();
}
},
beforeMount() {
async beforeMount() {
await this.loadBusinessInfo();
await this.loadCurrentUser();
this.loadData();
},
watch: {
itemsSelected: {
handler: function (val) {
this.sumSelected = 0;
this.itemsSelected.forEach((item) => {
if (typeof item.amount.valueOf() === "string") {
this.itemsSelected.forEach((code) => {
if (code && typeof code === 'string') {
const item = this.items.find(item => item.code === code);
if (item && item.amount) {
if (typeof item.amount === "string") {
this.sumSelected += parseInt(item.amount.replaceAll(",", ""));
} else {
this.sumSelected += item.amount;
}
}
}
});
},
deep: true
},
searchValue: {
handler() {
this.serverOptions.page = 1;
this.debouncedLoadData();
},
immediate: false,
},
currentTab: {
handler: function (val) {
if (this.searchValue == '') {
this.items = this.orgItems;
} else {
let temp = [];
this.orgItems.forEach((item) => {
if (item.person.nikename.includes(this.searchValue)) {
temp.push(item);
} else if (item.date.includes(this.searchValue)) {
temp.push(item);
} else if (item.amount.toString().includes(this.searchValue)) {
temp.push(item);
} else if (item.des.includes(this.searchValue)) {
temp.push(item);
} else if (item.code.includes(this.searchValue)) {
temp.push(item);
} else if (item.label) {
if (item.label.label.includes(this.searchValue)) {
temp.push(item);
}
}
});
this.items = temp;
}
this.loadData();
},
deep: false
}
}
}
});
</script>
<style scoped>

View file

@ -506,6 +506,7 @@ export default {
bid: {
maliyatafzode: 0
},
business: { requireTwoStepApproval: false, approvers: { buyInvoice: null } },
desSubmit: {
id: '',
des: ''
@ -871,6 +872,7 @@ export default {
//load business info
axios.post('/api/business/get/info/' + localStorage.getItem('activeBid')).then((response) => {
this.bid = response.data;
this.business = response.data || { requireTwoStepApproval: false, approvers: { buyInvoice: null } };
if (this.bid.maliyatafzode == 0) {
this.maliyatCheck = false;
}
@ -964,7 +966,8 @@ export default {
rows: this.items,
discountAll: this.data.discountAll,
transferCost: this.data.transferCost,
update: this.$route.params.id
update: this.$route.params.id,
business: this.business
}).then((response) => {
this.loading = false;
if (response.data.code == 0) {

View file

@ -57,6 +57,7 @@ export default defineComponent({
bid: {
legal_name: '',
},
business: { requireTwoStepApproval: false, approvers: { buyInvoice: null } },
item: {
doc: {
id: 0,
@ -90,6 +91,18 @@ export default defineComponent({
navigator.clipboard.writeText(this.shortlink_url);
this.copy_label = 'کپی شد !';
},
getApprovalStatusText(item) {
if (!this.business?.requireTwoStepApproval) return 'تایید دو مرحله‌ای غیرفعال';
if (item.isPreview) return 'در انتظار تایید';
if (item.isApproved) return 'تایید شده';
return 'تایید شده';
},
getApprovalStatusColor(item) {
if (!this.business?.requireTwoStepApproval) return 'default';
if (item.isPreview) return 'warning';
if (item.isApproved) return 'success';
return 'success';
},
loadData() {
this.loading = true;
this.commoditys = [];
@ -127,6 +140,7 @@ export default defineComponent({
});
});
axios.post('/api/business/get/info/' + localStorage.getItem('activeBid')).then((response) => {
this.business = response.data || { requireTwoStepApproval: false, approvers: { buyInvoice: null } };
this.bid = response.data;
});
axios.post("/api/printers/options/info").then((response) => {
@ -220,6 +234,11 @@ export default defineComponent({
</button>
<i class="fas fa-file-invoice-dollar"></i>
مشاهده فاکتور
<span v-if="business.requireTwoStepApproval" class="ms-2">
<v-chip :color="getApprovalStatusColor(item.doc)" size="small">
{{ getApprovalStatusText(item.doc) }}
</v-chip>
</span>
</h3>
<div class="block-options">
<archive-upload v-if="this.item.doc.id != 0" :docid="this.item.doc.id" doctype="buy" cat="buy"></archive-upload>

View file

@ -64,7 +64,7 @@
</v-toolbar>
<!-- Tabs for two-step approval -->
<div v-if="business.requireTwoStepApproval" class="px-2 pt-2">
<v-tabs v-model="currentTab" color="primary" density="comfortable">
<v-tabs v-model="currentTab" color="primary" density="comfortable" grow>
<v-tab value="approved">فاکتورهای تایید شده</v-tab>
<v-tab value="pending">فاکتورهای در انتظار تایید</v-tab>
</v-tabs>