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) { foreach ($docIds as $docId) {
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([ $document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'id' => $docId, 'code' => $docId,
'bid' => $business '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 private function canUserApproveDocument(User $user, Business $business, HesabdariDoc $document): bool
{ {
if ($user->getEmail() === $business->getOwner()->getEmail()) { if ($user->getEmail() === $business->getOwner()->getEmail()) {
@ -432,4 +700,13 @@ class ApprovalController extends AbstractController
return $business->getApproverSellInvoice() === $user->getEmail(); 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()); return $this->json($extractor->notFound());
} }
foreach ($params['items'] as $item) { foreach ($params['items'] as $item) {
if (!$item || !isset($item['code'])) {
continue;
}
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ $doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'], 'bid' => $acc['bid'],
'year' => $acc['year'], 'year' => $acc['year'],
@ -286,77 +289,177 @@ class BuyController extends AbstractController
return $this->json($extractor->operationSuccess()); 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 public function app_buy_docs_search(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{ {
$acc = $access->hasRole('buy'); $acc = $access->hasRole('buy');
if (!$acc) if (!$acc)
throw $this->createAccessDeniedException(); throw $this->createAccessDeniedException();
$params = []; $params = json_decode($request->getContent(), true) ?? [];
if ($content = $request->getContent()) { $searchTerm = $params['search'] ?? '';
$params = json_decode($content, true); $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'], if (!empty($types)) {
'year' => $acc['year'], $queryBuilder->andWhere('l.code IN (:types)')
'type' => 'buy', ->setParameter('types', $types);
'money' => $acc['money'] }
], [
'id' => 'DESC' // فیلدهای معتبر برای مرتب‌سازی توی دیتابیس
]); $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 = []; $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'],
'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; $mainRow = $entityManager->getRepository(HesabdariRow::class)
if ($item->getInvoiceLabel()) { ->createQueryBuilder('r')
$temp['label'] = [ ->where('r.doc = :docId')
'code' => $item->getInvoiceLabel()->getCode(), ->andWhere('r.person IS NOT NULL')
'label' => $item->getInvoiceLabel()->getLabel() ->setParameter('docId', $doc['id'])
]; ->setMaxResults(1)
} ->getQuery()
->getOneOrNullResult();
$item['person'] = $mainRow && $mainRow->getPerson() ? Explore::ExplorePerson($mainRow->getPerson()) : null;
$temp['relatedDocsCount'] = count($item->getRelatedDocs()); // محاسبه پرداختی‌ها
$pays = 0; $relatedDocs = $entityManager->getRepository(HesabdariDoc::class)
foreach ($item->getRelatedDocs() as $relatedDoc) { ->createQueryBuilder('rd')
$pays += $relatedDoc->getAmount(); ->select('SUM(rd.amount) as total_pays, COUNT(rd.id) as count_docs')
} ->innerJoin('rd.relatedDocs', 'rel')
$temp['relatedDocsPays'] = $pays; ->where('rel.id = :sourceDocId')
->andWhere('rd.bid = :bidId')
->setParameter('sourceDocId', $doc['id'])
->setParameter('bidId', $acc['bid']->getId())
->getQuery()
->getSingleResult();
$temp['commodities'] = []; $item['relatedDocsCount'] = (int) $relatedDocs['count_docs'];
foreach ($item->getHesabdariRows() as $item) { $item['relatedDocsPays'] = $relatedDocs['total_pays'] ?? 0;
if ($item->getRef()->getCode() == '51') {
$temp['discountAll'] = $item->getBs(); // محاسبه کالاها و تخفیف/هزینه حمل
} elseif ($item->getRef()->getCode() == '90') { $item['commodities'] = [];
$temp['transferCost'] = $item->getBd(); $item['discountAll'] = 0;
} $item['transferCost'] = 0;
if ($item->getCommodity()) {
$temp['commodities'][] = Explore::ExploreCommodity($item->getCommodity(), $item->getCommdityCount()); $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());
} }
} }
if (!array_key_exists('discountAll', $temp))
$temp['discountAll'] = 0;
if (!array_key_exists('transferCost', $temp))
$temp['transferCost'] = 0;
$dataTemp[] = $temp; $dataTemp[] = $item;
} }
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')] #[Route('/api/buy/posprinter/invoice', name: 'app_buy_posprinter_invoice')]
@ -425,6 +528,8 @@ class BuyController extends AbstractController
return $this->json(['id' => $pdfPid]); return $this->json(['id' => $pdfPid]);
} }
#[Route('/api/buy/print/invoice', name: 'app_buy_print_invoice')] #[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 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]); 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 // Approval fields
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true, options: ['default' => false])]
private ?bool $isPreview = null; private ?bool $isPreview = false;
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true, options: ['default' => true])]
private ?bool $isApproved = null; private ?bool $isApproved = true;
#[ORM\ManyToOne] #[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true)] #[ORM\JoinColumn(nullable: true)]

View file

@ -109,7 +109,11 @@ class HesabdariDocRepository extends ServiceEntityRepository
$qb = $this->createQueryBuilder('h'); $qb = $this->createQueryBuilder('h');
foreach ($criteria as $field => $value) { foreach ($criteria as $field => $value) {
$qb->andWhere("h.$field = :$field")->setParameter($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))'); $qb->andWhere('(h.isApproved = 1 OR (h.isApproved = 0 AND h.isPreview = 0))');
@ -129,7 +133,11 @@ class HesabdariDocRepository extends ServiceEntityRepository
$qb = $this->createQueryBuilder('h'); $qb = $this->createQueryBuilder('h');
foreach ($criteria as $field => $value) { foreach ($criteria as $field => $value) {
$qb->andWhere("h.$field = :$field")->setParameter($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) { if ($orderBy) {
@ -146,7 +154,11 @@ class HesabdariDocRepository extends ServiceEntityRepository
$qb = $this->createQueryBuilder('h'); $qb = $this->createQueryBuilder('h');
foreach ($criteria as $field => $value) { foreach ($criteria as $field => $value) {
$qb->andWhere("h.$field = :$field")->setParameter($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))'); $qb->andWhere('(h.isApproved = 1 OR (h.isApproved = 0 AND h.isPreview = 0))');
@ -175,4 +187,34 @@ class HesabdariDocRepository extends ServiceEntityRepository
->getQuery() ->getQuery()
->getResult(); ->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> </v-btn>
</template> </template>
<v-list> <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-subheader color="primary">تغییر برچسبها</v-list-subheader>
<v-list-item v-for="item in types" class="text-dark" :title="'تغییر به ' + item.label" @click="changeLabel(item)"> <v-list-item v-for="item in types" class="text-dark" :title="'تغییر به ' + item.label" @click="changeLabel(item)">
<template v-slot:prepend> <template v-slot:prepend>
@ -43,6 +54,14 @@
</v-menu> </v-menu>
</v-toolbar> </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 <v-text-field
hide-details hide-details
@ -88,48 +107,50 @@
</v-text-field> </v-text-field>
<!-- جدول اصلی --> <!-- جدول اصلی -->
<EasyDataTable <v-data-table-server v-model:items-per-page="serverOptions.rowsPerPage" v-model:page="serverOptions.page"
v-model:items-selected="itemsSelected" :headers="visibleHeaders" :items="displayItems" :items-length="displayTotal" :loading="loading"
table-class-name="customize-table" :no-data-text="$t('table.no_data')" v-model="itemsSelected" show-select class="elevation-1 data-table-wrapper" item-value="code"
show-index :max-height="tableHeight" :header-props="{ class: 'custom-header' }" @update:options="updateServerOptions"
alternating multi-sort>
:headers="headers" <template v-slot:item.operation="{ item }">
:items="items"
theme-color="#1d90ff"
header-text-direction="center"
body-text-direction="center"
rowsPerPageMessage="تعداد سطر"
emptyMessage="اطلاعاتی برای نمایش وجود ندارد"
rowsOfPageSeparatorMessage="از"
:loading="loading"
>
<template #item-operation="{ code }">
<v-menu> <v-menu>
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn variant="text" size="small" color="error" icon="mdi-menu" v-bind="props" /> <v-btn variant="text" size="small" color="error" icon="mdi-menu" v-bind="props" />
</template> </template>
<v-list> <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> <template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-file"></v-icon> <v-icon color="green-darken-4" icon="mdi-file"></v-icon>
</template> </template>
</v-list-item> </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> <template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-eye"></v-icon> <v-icon color="green-darken-4" icon="mdi-eye"></v-icon>
</template> </template>
</v-list-item> </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> <template v-slot:prepend>
<v-icon icon="mdi-file-pdf-box"></v-icon> <v-icon icon="mdi-file-pdf-box"></v-icon>
</template> </template>
</v-list-item> </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> <template v-slot:prepend>
<v-icon icon="mdi-file-edit"></v-icon> <v-icon icon="mdi-file-edit"></v-icon>
</template> </template>
</v-list-item> </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> <template v-slot:prepend>
<v-icon color="deep-orange-accent-4" icon="mdi-trash-can"></v-icon> <v-icon color="deep-orange-accent-4" icon="mdi-trash-can"></v-icon>
</template> </template>
@ -137,48 +158,56 @@
</v-list> </v-list>
</v-menu> </v-menu>
</template> </template>
<template #item-label="{ label }"> <template v-slot:item.label="{ item }">
<span v-if="label"> <span v-if="item.label">
<span v-if="label.code == 'payed'" class="text-success">{{ label.label }}</span> <span v-if="item.label.code == 'payed'" class="text-success">{{ item.label.label }}</span>
<span v-if="label.code == 'returned'" class="text-danger">{{ label.label }}</span> <span v-if="item.label.code == 'returned'" class="text-danger">{{ item.label.label }}</span>
<span v-if="label.code == 'accepted'" class="text-info">{{ label.label }}</span> <span v-if="item.label.code == 'accepted'" class="text-info">{{ item.label.label }}</span>
</span> </span>
</template> </template>
<template #item-des="{ des }"> <template v-slot:item.des="{ item }">
{{ des.replace("فاکتور خرید:", "") }} {{ item.des.replace("فاکتور خرید:", "") }}
</template> </template>
<template #item-relatedDocsCount="{ relatedDocsCount, relatedDocsPays }"> <template v-slot:item.relatedDocsCount="{ item }">
<span v-if="relatedDocsCount != '0'" class="text-success"> <span v-if="item.relatedDocsCount != '0'" class="text-success">
<v-icon small>mdi-currency-usd</v-icon> {{ $filters.formatNumber(item.relatedDocsPays) }}
{{ $filters.formatNumber(relatedDocsPays) }}
</span> </span>
</template> </template>
<template #item-amount="{ amount }"> <template v-slot:item.amount="{ item }">
<span class="text-dark"> <span class="text-dark">
{{ $filters.formatNumber(amount) }} {{ $filters.formatNumber(item.amount) }}
</span> </span>
</template> </template>
<template #item-transferCost="{ transferCost }"> <template v-slot:item.transferCost="{ item }">
<span class="text-dark"> <span class="text-dark">
{{ $filters.formatNumber(transferCost) }} {{ $filters.formatNumber(item.transferCost) }}
</span> </span>
</template> </template>
<template #item-discountAll="{ discountAll }"> <template v-slot:item.discountAll="{ item }">
<span class="text-dark"> <span class="text-dark">
{{ $filters.formatNumber(discountAll) }} {{ $filters.formatNumber(item.discountAll) }}
</span> </span>
</template> </template>
<template #item-person="{ person }"> <template v-slot:item.person="{ item }">
<router-link :to="'/acc/persons/card/view/' + person.code"> <router-link v-if="item.person" :to="'/acc/persons/card/view/' + item.person.code">
{{ person.nikename }} {{ 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> </router-link>
</template> </template>
<template #item-code="{ code }"> </v-data-table-server>
<router-link :to="'/acc/buy/view/' + code">
{{ code }}
</router-link>
</template>
</EasyDataTable>
<!-- مودال چاپ --> <!-- مودال چاپ -->
<v-dialog v-model="printModal" width="auto"> <v-dialog v-model="printModal" width="auto">
@ -236,48 +265,87 @@
<script> <script>
import axios from "axios"; import axios from "axios";
import Swal from "sweetalert2"; 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", name: "list",
data: () => ({ data() {
printModal: false, return {
printOptions: { currentTab: 'approved',
pays: true, printModal: false,
note: true, printOptions: {
bidInfo: true, pays: true,
taxInfo: true, note: true,
discountInfo: true, bidInfo: true,
selectedPrintCode: 0, taxInfo: true,
paper: 'A4-L' discountInfo: true,
selectedPrintCode: 0,
paper: 'A4-L'
},
paperSizes: [
{ title: 'A4 عمودی', value: 'A4' },
{ title: 'A4 افقی', value: 'A4-L' },
{ 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: false,
items: [],
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;
});
}, },
paperSizes: [ tableHeight() {
{ title: 'A4 عمودی', value: 'A4' }, return window.innerHeight - 200;
{ title: 'A4 افقی', value: 'A4-L' }, },
{ title: 'A5 عمودی', value: 'A5' }, displayItems() {
{ title: 'A5 افقی', value: 'A5-L' }, if (!this.business.requireTwoStepApproval) return this.items;
], return this.currentTab === 'pending' ? this.itemsPending : this.itemsApproved;
sumSelected: 0, },
sumTotal: 0, displayTotal() {
itemsSelected: [], if (!this.business.requireTwoStepApproval) return this.total;
searchValue: '', return this.currentTab === 'pending' ? this.totalPending : this.totalApproved;
types: [], },
loading: ref(true), },
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 },
]
}),
methods: { methods: {
openPrintModal(code) { openPrintModal(code) {
this.printOptions.selectedPrintCode = code; this.printOptions.selectedPrintCode = code;
@ -293,7 +361,7 @@ export default {
} else { } else {
this.loading = true; this.loading = true;
axios.post('/api/buy/label/change', { axios.post('/api/buy/label/change', {
'items': this.itemsSelected, 'items': this.itemsSelected.filter(item => item && typeof item === 'string').map(item => ({ code: item })),
'label': label 'label': label
}).then((response) => { }).then((response) => {
this.loading = false; 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() { filterTable() {
this.loading = true; this.loading = true;
let calcItems = []; let calcItems = [];
@ -350,27 +542,70 @@ export default {
} }
this.loading = false; this.loading = false;
}, },
loadData() { updateServerOptions(options) {
axios.post("/api/printers/options/info").then((response) => { this.serverOptions.page = options.page;
this.printOptions = response.data.buy; this.serverOptions.rowsPerPage = options.itemsPerPage;
}); this.serverOptions.sortBy = options.sortBy || [];
this.loadData();
},
debouncedLoadData: debounce(function () {
this.loadData();
}, 300),
axios.post('/api/invoice/types', { async loadData() {
type: 'buy' this.loading = true;
}).then((response) => { try {
this.types = response.data; if (!this.printOptions.selectedPrintCode) {
}); const printResponse = await axios.post("/api/printers/options/info");
this.printOptions = printResponse.data.buy || this.printOptions;
}
axios.post('/api/buy/docs/search', { const typesResponse = await axios.post('/api/invoice/types', { type: 'buy' });
type: 'buy' this.types = typesResponse.data.map(t => ({
}).then((response) => { ...t,
this.items = response.data; checked: this.types.find(x => x.code === t.code)?.checked ?? false
this.orgItems = response.data; }));
this.items.forEach((item) => {
this.sumTotal += parseInt(item.amount); 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,
}); });
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');
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; this.loading = false;
}); }
}, },
canEditItem(code) { canEditItem(code) {
this.loading = true; this.loading = true;
@ -405,7 +640,7 @@ export default {
if (result.isConfirmed) { if (result.isConfirmed) {
this.loading = true; this.loading = true;
axios.post('/api/accounting/remove/group', { axios.post('/api/accounting/remove/group', {
'items': this.itemsSelected 'items': this.itemsSelected.filter(item => item && typeof item === 'string')
}).then((response) => { }).then((response) => {
this.loading = false; this.loading = false;
if (response.data.result == 1) { 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(); this.loadData();
}, },
watch: { watch: {
itemsSelected: { itemsSelected: {
handler: function (val) { handler: function (val) {
this.sumSelected = 0; this.sumSelected = 0;
this.itemsSelected.forEach((item) => { this.itemsSelected.forEach((code) => {
if (typeof item.amount.valueOf() === "string") { if (code && typeof code === 'string') {
this.sumSelected += parseInt(item.amount.replaceAll(",", "")); const item = this.items.find(item => item.code === code);
} else { if (item && item.amount) {
this.sumSelected += item.amount; if (typeof item.amount === "string") {
this.sumSelected += parseInt(item.amount.replaceAll(",", ""));
} else {
this.sumSelected += item.amount;
}
}
} }
}); });
}, },
deep: true deep: true
}, },
searchValue: { searchValue: {
handler() {
this.serverOptions.page = 1;
this.debouncedLoadData();
},
immediate: false,
},
currentTab: {
handler: function (val) { handler: function (val) {
if (this.searchValue == '') { this.loadData();
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;
}
}, },
deep: false deep: false
} }
} }
} });
</script> </script>
<style scoped> <style scoped>

View file

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

View file

@ -57,6 +57,7 @@ export default defineComponent({
bid: { bid: {
legal_name: '', legal_name: '',
}, },
business: { requireTwoStepApproval: false, approvers: { buyInvoice: null } },
item: { item: {
doc: { doc: {
id: 0, id: 0,
@ -90,6 +91,18 @@ export default defineComponent({
navigator.clipboard.writeText(this.shortlink_url); navigator.clipboard.writeText(this.shortlink_url);
this.copy_label = 'کپی شد !'; 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() { loadData() {
this.loading = true; this.loading = true;
this.commoditys = []; this.commoditys = [];
@ -127,6 +140,7 @@ export default defineComponent({
}); });
}); });
axios.post('/api/business/get/info/' + localStorage.getItem('activeBid')).then((response) => { axios.post('/api/business/get/info/' + localStorage.getItem('activeBid')).then((response) => {
this.business = response.data || { requireTwoStepApproval: false, approvers: { buyInvoice: null } };
this.bid = response.data; this.bid = response.data;
}); });
axios.post("/api/printers/options/info").then((response) => { axios.post("/api/printers/options/info").then((response) => {
@ -220,6 +234,11 @@ export default defineComponent({
</button> </button>
<i class="fas fa-file-invoice-dollar"></i> <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> </h3>
<div class="block-options"> <div class="block-options">
<archive-upload v-if="this.item.doc.id != 0" :docid="this.item.doc.id" doctype="buy" cat="buy"></archive-upload> <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> </v-toolbar>
<!-- Tabs for two-step approval --> <!-- Tabs for two-step approval -->
<div v-if="business.requireTwoStepApproval" class="px-2 pt-2"> <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="approved">فاکتورهای تایید شده</v-tab>
<v-tab value="pending">فاکتورهای در انتظار تایید</v-tab> <v-tab value="pending">فاکتورهای در انتظار تایید</v-tab>
</v-tabs> </v-tabs>