diff --git a/hesabixCore/migrations/Version20250820232952.php b/hesabixCore/migrations/Version20250820232952.php new file mode 100644 index 0000000..ceaee74 --- /dev/null +++ b/hesabixCore/migrations/Version20250820232952.php @@ -0,0 +1,53 @@ +addSql(<<<'SQL' + ALTER TABLE import_workflow_payment CHANGE amount amount NUMERIC(15, 2) NOT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE storeroom_ticket ADD completed TINYINT(1) DEFAULT NULL, ADD completed_at DATETIME DEFAULT NULL, ADD completed_by_id INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE storeroom_ticket ADD CONSTRAINT FK_9B4CC0F785ECDE76 FOREIGN KEY (completed_by_id) REFERENCES user (id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_9B4CC0F785ECDE76 ON storeroom_ticket (completed_by_id) + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE import_workflow_payment CHANGE amount amount NUMERIC(15, 2) DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE storeroom_ticket DROP FOREIGN KEY FK_9B4CC0F785ECDE76 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_9B4CC0F785ECDE76 ON storeroom_ticket + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE storeroom_ticket DROP completed, DROP completed_at, DROP completed_by_id + SQL); + } +} diff --git a/hesabixCore/migrations/Version20250820233206.php b/hesabixCore/migrations/Version20250820233206.php new file mode 100644 index 0000000..821b86b --- /dev/null +++ b/hesabixCore/migrations/Version20250820233206.php @@ -0,0 +1,47 @@ +addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial ADD device_serial VARCHAR(255) DEFAULT NULL, ADD allocated_to_document_type VARCHAR(50) DEFAULT NULL, ADD allocated_by_id INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial ADD CONSTRAINT FK_1A5DC26F6802B588 FOREIGN KEY (allocated_by_id) REFERENCES user (id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1A5DC26F6802B588 ON plug_warranty_serial (allocated_by_id) + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial DROP FOREIGN KEY FK_1A5DC26F6802B588 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_1A5DC26F6802B588 ON plug_warranty_serial + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial DROP device_serial, DROP allocated_to_document_type, DROP allocated_by_id + SQL); + } +} diff --git a/hesabixCore/migrations/Version20250820235141.php b/hesabixCore/migrations/Version20250820235141.php new file mode 100644 index 0000000..a525e46 --- /dev/null +++ b/hesabixCore/migrations/Version20250820235141.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial DROP device_serial + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial ADD device_serial VARCHAR(255) DEFAULT NULL + SQL); + } +} diff --git a/hesabixCore/src/Controller/ApprovalController.php b/hesabixCore/src/Controller/ApprovalController.php index 6cf9686..865451b 100644 --- a/hesabixCore/src/Controller/ApprovalController.php +++ b/hesabixCore/src/Controller/ApprovalController.php @@ -8,6 +8,7 @@ use App\Entity\HesabdariRow; use App\Entity\Log; use App\Entity\Permission; use App\Entity\User; +use App\Entity\Year; use App\Service\Access; use App\Service\Log as LogService; use Doctrine\ORM\EntityManagerInterface; @@ -55,6 +56,10 @@ class ApprovalController extends AbstractController return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']); } + if (!$this->checkDocumentYear($ticket->getDoc(), $business, $entityManager)) { + return $this->json(['success' => false, 'message' => 'حواله مربوط به این سال مالی نیست']); + } + $ticket->setIsPreview(false); $ticket->setIsApproved(true); $ticket->setApprovedBy($user); @@ -117,6 +122,10 @@ class ApprovalController extends AbstractController return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']); } + if (!$this->checkDocumentYear($ticket->getDoc(), $business, $entityManager)) { + return $this->json(['success' => false, 'message' => 'حواله مربوط به این سال مالی نیست']); + } + $ticket->setIsPreview(true); $ticket->setIsApproved(false); $ticket->setApprovedBy(null); @@ -179,6 +188,10 @@ class ApprovalController extends AbstractController return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']); } + if (!$this->checkDocumentYear($document, $business, $entityManager)) { + return $this->json(['success' => false, 'message' => 'فاکتور مربوط به این سال مالی نیست']); + } + $document->setIsPreview(false); $document->setIsApproved(true); $document->setApprovedBy($user); @@ -262,6 +275,10 @@ class ApprovalController extends AbstractController return $this->json(['success' => false, 'message' => 'فاکتور فروش تایید شده است']); } + if (!$this->checkDocumentYear($document, $business, $entityManager)) { + return $this->json(['success' => false, 'message' => 'فاکتور مربوط به این سال مالی نیست']); + } + $document->setIsPreview(false); $document->setIsApproved(true); $document->setApprovedBy($user); @@ -338,6 +355,10 @@ class ApprovalController extends AbstractController return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']); } + if (!$this->checkDocumentYear($document, $business, $entityManager)) { + return $this->json(['success' => false, 'message' => 'فاکتور مربوط به این سال مالی نیست']); + } + $document->setIsPreview(true); $document->setIsApproved(false); $document->setApprovedBy(null); @@ -413,6 +434,10 @@ class ApprovalController extends AbstractController return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']); } + if (!$this->checkDocumentYear($document, $business, $entityManager)) { + return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']); + } + $document->setIsPreview(false); $document->setIsApproved(true); $document->setApprovedBy($user); @@ -483,6 +508,10 @@ class ApprovalController extends AbstractController return $this->json(['success' => false, 'message' => 'فاکتور خرید تایید شده است']); } + if (!$this->checkDocumentYear($document, $business, $entityManager)) { + return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']); + } + $document->setIsPreview(false); $document->setIsApproved(true); $document->setApprovedBy($user); @@ -547,6 +576,10 @@ class ApprovalController extends AbstractController return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']); } + if (!$this->checkDocumentYear($document, $business, $entityManager)) { + return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']); + } + $document->setIsPreview(true); $document->setIsApproved(false); $document->setApprovedBy(null); @@ -617,6 +650,10 @@ class ApprovalController extends AbstractController return $this->json(['success' => false, 'message' => 'فاکتور خرید تایید نشده است']); } + if (!$this->checkDocumentYear($document, $business, $entityManager)) { + return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']); + } + $document->setIsPreview(true); $document->setIsApproved(false); $document->setApprovedBy(null); @@ -646,6 +683,15 @@ class ApprovalController extends AbstractController } } + private function checkDocumentYear(HesabdariDoc $document, Business $business, EntityManagerInterface $entityManager): bool + { + $year = $entityManager->getRepository(Year::class)->findOneBy([ + 'bid' => $business, + 'head' => true + ]); + return $document->getYear()->getId() == $year->getId(); + } + private function canUserApproveDocument(User $user, Business $business, string $documentType): bool { if ($user->getEmail() === $business->getOwner()->getEmail()) { diff --git a/hesabixCore/src/Controller/BusinessController.php b/hesabixCore/src/Controller/BusinessController.php index 18d7fbb..c58bcde 100644 --- a/hesabixCore/src/Controller/BusinessController.php +++ b/hesabixCore/src/Controller/BusinessController.php @@ -583,10 +583,10 @@ class BusinessController extends AbstractController 'plugGhestaManager' => true, 'plugTaxSettings' => true, 'plugWarranty' => true, + 'plugImportWorkflow' => true, 'inquiry' => true, 'ai' => true, - 'warehouseManager' => true, - 'importWorkflow' => true, + 'storehelper' => true, ]; } elseif ($perm) { $result = [ @@ -632,18 +632,11 @@ class BusinessController extends AbstractController 'plugGhestaManager' => $perm->isPlugGhestaManager(), 'plugTaxSettings' => $perm->isPlugTaxSettings(), 'plugWarranty' => $perm->isPlugWarrantyManager(), + 'plugImportWorkflow' => $perm->isImportWorkflow(), 'inquiry' => $perm->isInquiry(), 'ai' => $perm->isAi(), - 'warehouseManager' => $perm->isWarehouseManager(), - 'importWorkflow' => $perm->isImportWorkflow(), + 'storehelper' => $perm->isStorehelper() ]; - - if ($perm->isWarehouseManager()) { - $result['commodity'] = true; - $result['store'] = true; - $result['plugWarranty'] = true; - $result['permission'] = true; - } } return $this->json($result); } @@ -714,10 +707,10 @@ class BusinessController extends AbstractController $perm->setPlugGhestaManager($params['plugGhestaManager']); $perm->setPlugWarrantyManager($params['plugWarranty'] ?? false); $perm->setPlugTaxSettings($params['plugTaxSettings']); + $perm->setImportWorkflow($params['plugImportWorkflow'] ?? false); $perm->setInquiry($params['inquiry']); $perm->setAi($params['ai']); - $perm->setWarehouseManager($params['warehouseManager'] ?? false); - $perm->setImportWorkflow($params['importWorkflow'] ?? false); + $perm->setStorehelper($params['storehelper'] ?? false); $entityManager->persist($perm); $entityManager->flush(); $log->insert('تنظیمات پایه', 'ویرایش دسترسی‌های کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business); diff --git a/hesabixCore/src/Controller/Plugins/PlugWarrantyController.php b/hesabixCore/src/Controller/Plugins/PlugWarrantyController.php index ccb9735..8c6f9a2 100644 --- a/hesabixCore/src/Controller/Plugins/PlugWarrantyController.php +++ b/hesabixCore/src/Controller/Plugins/PlugWarrantyController.php @@ -71,9 +71,9 @@ class PlugWarrantyController extends AbstractController $serials = $entityManager->getRepository(PlugWarrantySerial::class)->createQueryBuilder('s') ->andWhere('s.business = :bid') - ->andWhere('s.allocatedToDocumentId = :docId') + ->andWhere('s.activationTicketCode = :code') ->setParameter('bid', $acc['bid']) - ->setParameter('docId', $doc->getId()) + ->setParameter('code', $code) ->getQuery() ->getResult(); @@ -81,6 +81,7 @@ class PlugWarrantyController extends AbstractController $commodity = $s->getCommodity(); return [ 'serialNumber' => $s->getSerialNumber(), + 'commoditySerial' => $s->getCommoditySerial(), 'commodity' => $commodity ? [ 'id' => $commodity->getId(), 'name' => $commodity->getName(), diff --git a/hesabixCore/src/Controller/StoreroomController.php b/hesabixCore/src/Controller/StoreroomController.php index ebe968b..a1e2f2d 100644 --- a/hesabixCore/src/Controller/StoreroomController.php +++ b/hesabixCore/src/Controller/StoreroomController.php @@ -60,16 +60,18 @@ class StoreroomController extends AbstractController public function uploadTicketAttachment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): JsonResponse { $acc = $access->hasRole('store'); - if (!$acc) throw $this->createAccessDeniedException(); - $ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid'=>$acc['bid'],'code'=>$code]); - if (!$ticket) throw $this->createNotFoundException('حواله یافت نشد'); + if (!$acc) + throw $this->createAccessDeniedException(); + $ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid' => $acc['bid'], 'code' => $code]); + if (!$ticket) + throw $this->createNotFoundException('حواله یافت نشد'); $file = $request->files->get('file'); if (!$file) { - return $this->json(['result'=>-1,'message'=>'فایل ارسال نشده است'], 400); + return $this->json(['result' => -1, 'message' => 'فایل ارسال نشده است'], 400); } - - $stored = $storage->store($file, (string)$acc['bid']->getId(), 'storeroom_attachments'); + + $stored = $storage->store($file, (string) $acc['bid']->getId(), 'storeroom_attachments'); $archive = new ArchiveFile(); $archive->setBid($acc['bid']); @@ -82,33 +84,35 @@ class StoreroomController extends AbstractController $archive->setDes($request->request->get('des')); $archive->setRelatedDocType('storeroom_ticket'); $archive->setRelatedDocCode($ticket->getCode()); - $archive->setFileSize($stored['size'] !== null ? (string)$stored['size'] : null); + $archive->setFileSize($stored['size'] !== null ? (string) $stored['size'] : null); $entityManager->persist($archive); $entityManager->flush(); - return $this->json(['result'=>0]); + return $this->json(['result' => 0]); } #[Route('/api/storeroom/ticket/attachments/{code}', name: 'app_storeroom_ticket_list_attachments', methods: ['GET'])] public function listTicketAttachments(string $code, Access $access, EntityManagerInterface $entityManager): JsonResponse { $acc = $access->hasRole('store'); - if (!$acc) throw $this->createAccessDeniedException(); - $ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid'=>$acc['bid'],'code'=>$code]); - if (!$ticket) throw $this->createNotFoundException('حواله یافت نشد'); + if (!$acc) + throw $this->createAccessDeniedException(); + $ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid' => $acc['bid'], 'code' => $code]); + if (!$ticket) + throw $this->createNotFoundException('حواله یافت نشد'); $items = $entityManager->getRepository(ArchiveFile::class)->findBy([ - 'bid'=>$acc['bid'], - 'relatedDocType'=>'storeroom_ticket', - 'relatedDocCode'=>$ticket->getCode() - ], ['id'=>'DESC']); - return $this->json(array_map(function(ArchiveFile $a){ + 'bid' => $acc['bid'], + 'relatedDocType' => 'storeroom_ticket', + 'relatedDocCode' => $ticket->getCode() + ], ['id' => 'DESC']); + return $this->json(array_map(function (ArchiveFile $a) { return [ - 'id'=>$a->getId(), - 'filename'=>$a->getFilename(), - 'fileType'=>$a->getFileType(), - 'fileSize'=>$a->getFileSize(), - 'des'=>$a->getDes(), - 'dateSubmit'=>$a->getDateSubmit(), + 'id' => $a->getId(), + 'filename' => $a->getFilename(), + 'fileType' => $a->getFileType(), + 'fileSize' => $a->getFileSize(), + 'des' => $a->getDes(), + 'dateSubmit' => $a->getDateSubmit(), ]; }, $items)); } @@ -117,12 +121,13 @@ class StoreroomController extends AbstractController public function downloadTicketAttachment(int $id, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): Response { $acc = $access->hasRole('store'); - if (!$acc) throw $this->createAccessDeniedException(); + if (!$acc) + throw $this->createAccessDeniedException(); $a = $entityManager->getRepository(ArchiveFile::class)->find($id); if (!$a || $a->getBid()->getId() !== $acc['bid']->getId()) { throw $this->createNotFoundException('فایل یافت نشد'); } - $abs = $storage->absolutePath((string)$a->getFilename()); + $abs = $storage->absolutePath((string) $a->getFilename()); if (!is_file($abs) || !is_readable($abs)) { throw $this->createNotFoundException('فایل موجود نیست'); } @@ -201,7 +206,7 @@ class StoreroomController extends AbstractController * @throws ReflectionException */ #[Route('/api/storeroom/docs/get', name: 'app_storeroom_get_docs')] - public function app_storeroom_get_docs(Provider $provider,Extractor $extractor, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse + public function app_storeroom_get_docs(Provider $provider, Extractor $extractor, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse { $acc = $access->hasRole('store'); if (!$acc) @@ -466,10 +471,8 @@ class StoreroomController extends AbstractController if ($content = $request->getContent()) { $params = json_decode($content, true); } - //check parameters exist if ((!array_key_exists('ticket', $params)) || (!array_key_exists('items', $params)) || (!array_key_exists('doc', $params))) $this->createNotFoundException(); - //going to save $doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ 'id' => $params['doc']['id'], 'bid' => $acc['bid'], @@ -479,14 +482,11 @@ class StoreroomController extends AbstractController throw $this->createNotFoundException('سند یافت نشد'); if ($doc->getBid()->getId() != $acc['bid']->getId()) throw $this->createAccessDeniedException('دسترسی به این سند را ندارید.'); - //find transfer type if (!array_key_exists('transferType', $params['ticket'])) throw $this->createNotFoundException('نوع انتقال یافت نشد'); $transferType = $entityManager->getRepository(StoreroomTransferType::class)->find($params['ticket']['transferType']['id']); if (!$transferType) throw $this->createNotFoundException('نوع انتقال یافت نشد'); - - //find storeroom if (!array_key_exists('store', $params['ticket'])) throw $this->createNotFoundException('انبار یافت نشد'); $storeroom = $entityManager->getRepository(Storeroom::class)->find($params['ticket']['store']['id']); @@ -494,7 +494,6 @@ class StoreroomController extends AbstractController throw $this->createNotFoundException('انبار یافت نشد'); elseif ($storeroom->getBid()->getId() != $acc['bid']->getId()) throw $this->createAccessDeniedException('دسترسی به این انبار ممکن نیست!'); - //find person if (!array_key_exists('person', $params['ticket'])) throw $this->createNotFoundException('طرف حساب یافت نشد'); $person = $entityManager->getRepository(Person::class)->find($params['ticket']['person']['id']); @@ -502,7 +501,6 @@ class StoreroomController extends AbstractController throw $this->createNotFoundException('طرف حساب یافت نشد'); elseif ($person->getBid()->getId() != $acc['bid']->getId()) throw $this->createAccessDeniedException('دسترسی به این طرف حساب ممکن نیست!'); - $ticket = new StoreroomTicket(); $ticket->setSubmitter($this->getUser()); $ticket->setDate($params['ticket']['date']); @@ -515,7 +513,9 @@ class StoreroomController extends AbstractController $ticket->setCode($provider->getAccountingCode($acc['bid'], 'storeroom')); $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; $rand = ''; - for ($i = 0; $i < 8; $i++) { $rand .= $alphabet[random_int(0, strlen($alphabet)-1)]; } + for ($i = 0; $i < 8; $i++) { + $rand .= $alphabet[random_int(0, strlen($alphabet) - 1)]; + } $ticket->setActivationCode($rand); $ticket->setReceiver($params['ticket']['receiver']); $ticket->setTransferType($transferType); @@ -529,32 +529,25 @@ class StoreroomController extends AbstractController $ticket->setImportWorkflowCode($params['ticket']['importWorkflowCode']); } $entityManager->persist($ticket); - //$entityManager->flush(); - - //going to save rows $docRows = $entityManager->getRepository(HesabdariRow::class)->findBy([ 'doc' => $doc ]); - - // Determine if warranty serials are required based on flag or provided lines - $hasSerialLines = false; - foreach (($params['items'] ?? []) as $it) { - if (!empty($it['serialLines']) && is_array($it['serialLines'])) { $hasSerialLines = true; break; } - } - $requireWarrantySerial = (isset($params['ticket']['requireWarrantySerial']) && $params['ticket']['requireWarrantySerial'] === true) || $hasSerialLines; + $requireWarrantySerial = ( + isset($params['ticket']['requireWarrantySerial']) + && $params['ticket']['requireWarrantySerial'] === true + && $pluginService->isActive('warranty', $acc['bid']) + ); if ($requireWarrantySerial) { - if (!$pluginService->isActive('warranty', $acc['bid'])) { - return $this->json(['result' => -5, 'message' => 'افزونه گارانتی فعال نیست'], 403); - } - // Validate counts up-front foreach ($params['items'] as $item) { $lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : []; - if ((int)($item['ticketCount'] ?? 0) > 0 && count($lines) < (int)$item['ticketCount']) { - return $this->json(['result' => -3, 'message' => 'تعداد سریال/گارانتی با تعداد حواله همخوانی ندارد'], 400); + if ((int) ($item['ticketCount'] ?? 0) > 0 && count($lines) < (int) $item['ticketCount']) { + return $this->json([ + 'result' => -3, + 'message' => 'تعداد سریال/گارانتی با تعداد حواله همخوانی ندارد' + ], 400); } } } - foreach ($params['items'] as $item) { $row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([ 'bid' => $acc['bid'], @@ -565,7 +558,6 @@ class StoreroomController extends AbstractController throw $this->createNotFoundException('کالا یافت نشد!'); if (!$row->getCommodity()) throw $this->createNotFoundException('کالا یافت نشد!'); - //check row count not upper ticket count if ($row->getCommdityCount() < $item['ticketCount']) throw $this->createNotFoundException('تعداد کالای اضافه شده بیشتر از تعداد کالا در فاکتور است.'); $ticketItem = new StoreroomItem(); @@ -578,21 +570,17 @@ class StoreroomController extends AbstractController $ticketItem->setCommodity($row->getCommodity()); $ticketItem->setType($item['type']); $entityManager->persist($ticketItem); - - // Bind warranty serials per item if provided + $lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : []; if ($requireWarrantySerial) { - $lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : []; - if ((int)$item['ticketCount'] > 0) { - // Ensure we have an id to bind to + if ((int) $item['ticketCount'] > 0) { $entityManager->flush(); - $lines = array_slice($lines, 0, (int)$item['ticketCount']); + $lines = array_slice($lines, 0, (int) $item['ticketCount']); foreach ($lines as $ln) { $warrantyCode = $ln['warranty'] ?? null; $deviceSerial = $ln['serial'] ?? null; if (!$warrantyCode) { return $this->json(['result' => -4, 'message' => 'کد گارانتی ارسال نشده است'], 400); } - /** @var PlugWarrantySerial|null $serial */ $serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ 'business' => $acc['bid'], 'serialNumber' => $warrantyCode, @@ -613,27 +601,51 @@ class StoreroomController extends AbstractController $entityManager->persist($serial); } } + } else { + if (!empty($lines)) { + $entityManager->flush(); + foreach ($lines as $ln) { + $warrantyCode = $ln['warranty'] ?? null; + $deviceSerial = $ln['serial'] ?? null; + if (!$warrantyCode) { + continue; + } + $serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ + 'business' => $acc['bid'], + 'serialNumber' => $warrantyCode, + 'commodity' => $row->getCommodity(), + ]); + if (!$serial || $serial->getStatus() !== PlugWarrantySerial::STATUS_AVAILABLE) { + continue; + } + $serial->setStatus(PlugWarrantySerial::STATUS_CONSUMED); + $serial->setCommoditySerial($deviceSerial); + $serial->setBuyer($person); + $serial->setAllocatedToDocumentId($doc->getId()); + $serial->setAllocatedAt(new \DateTimeImmutable()); + $serial->setBoundToItemId($ticketItem->getId()); + $serial->setBoundAt(new \DateTimeImmutable()); + $serial->setActivationTicketCode($ticket->getCode()); + $serial->setActivationTicketSecret($ticket->getActivationCode()); + $entityManager->persist($serial); + } + } } - } - $entityManager->flush(); - $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']); - $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false; + $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool) $business->isRequireTwoStepApproval() : false; if ($businessRequire) { $ticket->setIsPreview(true); $ticket->setIsApproved(false); - $ticket->setApprovedBy(null); // هنوز تأیید نشده + $ticket->setApprovedBy(null); } else { $ticket->setIsPreview(false); $ticket->setIsApproved(true); - $ticket->setApprovedBy($this->getUser()); // تأیید شده توسط کاربر فعلی + $ticket->setApprovedBy($this->getUser()); } - //save logs $log->insert('انبارداری', 'حواله انبار با شماره ' . $ticket->getCode() . ' اضافه / ویرایش شد.', $this->getUser(), $acc['bid']); if ($pluginService->isActive('accpro', $acc['bid'])) { - //notification to person if ($params['ticket']['sms'] == true) { $ticket->setCanShare(true); $entityManager->persist($ticket); @@ -676,7 +688,6 @@ class StoreroomController extends AbstractController 3 ); } - if ($smsres == 2) { return $this->json([ 'result' => 2 @@ -684,7 +695,6 @@ class StoreroomController extends AbstractController } } } - return $this->json([ 'result' => 0 ]); @@ -698,7 +708,7 @@ class StoreroomController extends AbstractController throw $this->createAccessDeniedException(); $params = json_decode($request->getContent() ?: '{}', true); $status = $params['status'] ?? null; // in_progress|done|rejected|approved|pending_approval - if (!in_array($status, ['in_progress','done','rejected','approved','pending_approval'])) { + if (!in_array($status, ['in_progress', 'done', 'rejected', 'approved', 'pending_approval'])) { return $this->json(['result' => -1, 'message' => 'وضعیت نامعتبر'], 400); } $ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([ @@ -729,7 +739,7 @@ class StoreroomController extends AbstractController $criteria['status'] = $status; } $tickets = $entityManager->getRepository(StoreroomTicket::class)->findBy($criteria, ['date' => 'DESC']); - return $this->json(array_map(function(StoreroomTicket $t){ + return $this->json(array_map(function (StoreroomTicket $t) { return [ 'code' => $t->getCode(), 'date' => $t->getDate(), @@ -764,7 +774,8 @@ class StoreroomController extends AbstractController 'getDoc', 'getTypeString', 'isPreview', - 'isApproved' + 'isApproved', + 'isCompleted' ], 2); foreach ($result as $key => &$ticket) { @@ -779,6 +790,16 @@ class StoreroomController extends AbstractController } else { $ticket['approvedBy'] = null; } + if ($ticketEntity->getCompletedBy()) { + $completedBy = $ticketEntity->getCompletedBy(); + $ticket['completedBy'] = [ + 'id' => $completedBy->getId(), + 'fullName' => $completedBy->getFullName(), + 'email' => $completedBy->getEmail() + ]; + } else { + $ticket['completedBy'] = null; + } } return $this->json($result); @@ -799,7 +820,7 @@ class StoreroomController extends AbstractController //get items $items = $entityManager->getRepository(StoreroomItem::class)->findBy(['ticket' => $ticket]); $res = []; - $res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString', 'isPreview', 'isApproved'], 2); + $res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString', 'isPreview', 'isApproved', 'isCompleted'], 2); $res['transferType'] = $provider->Entity2ArrayJustIncludes($ticket->getTransferType(), ['getName'], 0); $res['person'] = $provider->Entity2ArrayJustIncludes($ticket->getPerson(), ['getKeshvar', 'getOstan', 'getShahr', 'getAddress', 'getNikename', 'getCodeeghtesadi', 'getPostalcode', 'getName', 'getTel', 'getSabt'], 0); //get rows @@ -916,9 +937,9 @@ class StoreroomController extends AbstractController } else { $title = 'حواله خروج از انبار'; } - + $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']); - $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false; + $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool) $business->isRequireTwoStepApproval() : false; if ($businessRequire && $doc->isApproved() !== true && $doc->isPreview() == true) { if ($doc->isPreview()) { return $this->json(['result' => -10, 'message' => 'حواله هنوز تایید نشده است'], 403); @@ -973,4 +994,103 @@ class StoreroomController extends AbstractController ); return $this->json(['id' => $pdfPid]); } + + #[Route('/api/storeroom/ticket/complete/{id}', name: 'app_storeroom_ticket_complete', methods: ['POST'])] + public function app_storeroom_ticket_complete(string $id, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, PluginService $pluginService): JsonResponse + { + $acc = $access->hasRole('store'); + if (!$acc) + throw $this->createAccessDeniedException(); + + $params = []; + if ($content = $request->getContent()) { + $params = json_decode($content, true); + } + + $ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([ + 'code' => $id, + 'bid' => $acc['bid'] + ]); + if (!$ticket) + throw $this->createNotFoundException('حواله یافت نشد'); + + $requireWarrantySerial = ( + isset($params['requireWarrantySerial']) + && $params['requireWarrantySerial'] === true + && $pluginService->isActive('warranty', $acc['bid']) + ); + + if ($pluginService->isActive('warranty', $acc['bid'])) { + $warrantyAllocations = $params['warrantyAllocations'] ?? []; + foreach ($warrantyAllocations as $allocation) { + $commodityId = $allocation['commodityId'] ?? null; + $warrantyLines = $allocation['warrantyLines'] ?? []; + if (!$commodityId || empty($warrantyLines)) { + if ($requireWarrantySerial) { + return $this->json(['result' => -3, 'message' => 'سریال گارانتی برای کالا ارسال نشده است'], 400); + } + continue; + } + $commodity = $entityManager->getRepository(Commodity::class)->find($commodityId); + if (!$commodity) { + if ($requireWarrantySerial) { + return $this->json(['result' => -3, 'message' => 'کالا معتبر نیست برای گارانتی'], 400); + } + continue; + } + foreach ($warrantyLines as $line) { + $warrantySerial = $line['warrantySerial'] ?? null; + $deviceSerial = $line['serialNumber'] ?? null; + $isBeforeAllocated = $line['isBeforeAllocated'] ?? false; + if (!$warrantySerial) { + if ($requireWarrantySerial) { + return $this->json(['result' => -4, 'message' => 'کد گارانتی ارسال نشده است'], 400); + } + continue; + } + if ($isBeforeAllocated) { + continue; + } + $warrantySerialEntity = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ + 'business' => $acc['bid'], + 'serialNumber' => $warrantySerial, + 'commodity' => $commodity, + 'status' => PlugWarrantySerial::STATUS_AVAILABLE + ]); + if (!$warrantySerialEntity) { + if ($requireWarrantySerial) { + return $this->json(['result' => -2, 'message' => "گارانتی {$warrantySerial} یافت نشد یا در دسترس نیست"], 400); + } + continue; + } + $warrantySerialEntity->setStatus(PlugWarrantySerial::STATUS_ALLOCATED); + $warrantySerialEntity->setAllocatedToDocumentId($ticket->getId()); + $warrantySerialEntity->setActivationTicketCode($ticket->getCode()); + $warrantySerialEntity->setActivationTicketSecret($ticket->getActivationCode()); + $warrantySerialEntity->setAllocatedToDocumentType('storeroom_ticket'); + $warrantySerialEntity->setAllocatedAt(new \DateTimeImmutable()); + $warrantySerialEntity->setAllocatedBy($this->getUser()); + if ($deviceSerial) { + $warrantySerialEntity->setCommoditySerial($deviceSerial); + } + $entityManager->persist($warrantySerialEntity); + } + } + } + + $ticket->setCompleted(true); + $ticket->setCompletedAt(new \DateTimeImmutable()); + $ticket->setCompletedBy($this->getUser()); + + $entityManager->persist($ticket); + $entityManager->flush(); + + $log->insert('انبارداری', 'پروسه حواله انبار با شماره ' . $ticket->getCode() . ' تکمیل شد.', $this->getUser(), $acc['bid']); + + return $this->json([ + 'result' => 0, + 'message' => 'پروسه با موفقیت تکمیل شد' + ]); + } + } diff --git a/hesabixCore/src/Entity/Permission.php b/hesabixCore/src/Entity/Permission.php index 48ac1a0..1c364e6 100644 --- a/hesabixCore/src/Entity/Permission.php +++ b/hesabixCore/src/Entity/Permission.php @@ -142,7 +142,7 @@ class Permission private ?bool $ai = null; #[ORM\Column(nullable: true)] - private ?bool $warehouseManager = null; + private ?bool $storehelper = null; #[ORM\Column(nullable: true)] private ?bool $importWorkflow = null; @@ -656,14 +656,14 @@ class Permission return $this; } - public function isWarehouseManager(): ?bool + public function isStorehelper(): ?bool { - return $this->warehouseManager; + return $this->storehelper; } - public function setWarehouseManager(?bool $warehouseManager): static + public function setStorehelper(?bool $storehelper): static { - $this->warehouseManager = $warehouseManager; + $this->storehelper = $storehelper; return $this; } diff --git a/hesabixCore/src/Entity/PlugWarrantySerial.php b/hesabixCore/src/Entity/PlugWarrantySerial.php index a557047..9bf1b14 100644 --- a/hesabixCore/src/Entity/PlugWarrantySerial.php +++ b/hesabixCore/src/Entity/PlugWarrantySerial.php @@ -82,6 +82,12 @@ class PlugWarrantySerial #[ORM\ManyToOne] private ?Person $buyer = null; + #[ORM\Column(type: 'string', length: 50, nullable: true)] + private ?string $allocatedToDocumentType = null; + + #[ORM\ManyToOne] + private ?User $allocatedBy = null; + #[ORM\Column(type: 'string', length: 32, nullable: true)] private ?string $activationTicketCode = null; @@ -152,6 +158,12 @@ class PlugWarrantySerial public function getBuyer(): ?Person { return $this->buyer; } public function setBuyer(?Person $buyer): self { $this->buyer = $buyer; return $this; } + public function getAllocatedToDocumentType(): ?string { return $this->allocatedToDocumentType; } + public function setAllocatedToDocumentType(?string $type): self { $this->allocatedToDocumentType = $type; return $this; } + + public function getAllocatedBy(): ?User { return $this->allocatedBy; } + public function setAllocatedBy(?User $user): self { $this->allocatedBy = $user; return $this; } + public function getActivationTicketCode(): ?string { return $this->activationTicketCode; } public function setActivationTicketCode(?string $code): self { $this->activationTicketCode = $code; return $this; } public function getActivationTicketSecret(): ?string { return $this->activationTicketSecret; } diff --git a/hesabixCore/src/Entity/StoreroomTicket.php b/hesabixCore/src/Entity/StoreroomTicket.php index 728e72e..70cd260 100644 --- a/hesabixCore/src/Entity/StoreroomTicket.php +++ b/hesabixCore/src/Entity/StoreroomTicket.php @@ -96,6 +96,17 @@ class StoreroomTicket #[ORM\JoinColumn(nullable: true)] private ?User $approvedBy = null; + // Completion fields + #[ORM\Column(nullable: true)] + private ?bool $completed = null; + + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + private ?\DateTimeImmutable $completedAt = null; + + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: true)] + private ?User $completedBy = null; + public function __construct() { $this->storeroomItems = new ArrayCollection(); @@ -409,4 +420,38 @@ class StoreroomTicket $this->approvedBy = $approvedBy; return $this; } + + // Completion methods + public function isCompleted(): ?bool + { + return $this->completed; + } + + public function setCompleted(?bool $completed): static + { + $this->completed = $completed; + return $this; + } + + public function getCompletedAt(): ?\DateTimeImmutable + { + return $this->completedAt; + } + + public function setCompletedAt(?\DateTimeImmutable $completedAt): static + { + $this->completedAt = $completedAt; + return $this; + } + + public function getCompletedBy(): ?User + { + return $this->completedBy; + } + + public function setCompletedBy(?User $completedBy): static + { + $this->completedBy = $completedBy; + return $this; + } } diff --git a/hesabixCore/src/Service/Access.php b/hesabixCore/src/Service/Access.php index c519049..88e4024 100644 --- a/hesabixCore/src/Service/Access.php +++ b/hesabixCore/src/Service/Access.php @@ -137,7 +137,7 @@ class Access 'user'=>$this->user ]); - if($warehousePermission && $warehousePermission->isWarehouseManager()){ + if($warehousePermission && $warehousePermission->isStorehelper()){ $warehouseRoles = ['commodity', 'store', 'plugWarrantyManager']; if(in_array($roll, $warehouseRoles)){ return $accessArray; diff --git a/webUI/src/components/plugins/warranty/SerialDialog.vue b/webUI/src/components/plugins/warranty/SerialDialog.vue index 71bccef..5e164bf 100644 --- a/webUI/src/components/plugins/warranty/SerialDialog.vue +++ b/webUI/src/components/plugins/warranty/SerialDialog.vue @@ -12,35 +12,32 @@ - diff --git a/webUI/src/components/widgets/BarcodeScanner.vue b/webUI/src/components/widgets/BarcodeScanner.vue new file mode 100644 index 0000000..5883c57 --- /dev/null +++ b/webUI/src/components/widgets/BarcodeScanner.vue @@ -0,0 +1,199 @@ + + + + + \ No newline at end of file diff --git a/webUI/src/i18n/fa_lang.ts b/webUI/src/i18n/fa_lang.ts index 34296ff..5e8ed7b 100755 --- a/webUI/src/i18n/fa_lang.ts +++ b/webUI/src/i18n/fa_lang.ts @@ -153,6 +153,7 @@ const fa_lang = { storeroom: "انبار", storeroom_title: "انبار‌داری", storeroom_ticket: "حواله انبار", + storeroom_ticket_helper: "کمک انباردار", storerooms: "انبار‌ها", commodity_exist_count: "موجودی کالا", inventory: "موجودی کالا", diff --git a/webUI/src/router/index.ts b/webUI/src/router/index.ts index 095132d..4f3d94c 100755 --- a/webUI/src/router/index.ts +++ b/webUI/src/router/index.ts @@ -974,12 +974,24 @@ const router = createRouter({ component: () => import('../views/acc/storeroom/io/ticketList.vue'), }, + { + path: 'storeroom/tickets/list/helper', + name: 'storeroom_tickets_list_helper', + component: () => + import('../views/acc/storeroom/io/ticketListHelper.vue'), + }, { path: 'storeroom/ticket/view/:id', name: 'storeroom_ticket_view', component: () => import('../views/acc/storeroom/io/view.vue'), }, + { + path: 'storeroom/ticket/complete/:id', + name: 'storeroom_ticket_complete', + component: () => + import('../views/acc/storeroom/io/complete.vue'), + }, { path: 'storeroom/new/ticket/buy/:doc/:storeID', name: 'storeroom_new_ticket_buy', diff --git a/webUI/src/views/acc/App.vue b/webUI/src/views/acc/App.vue index 80b531e..ebf5f26 100755 --- a/webUI/src/views/acc/App.vue +++ b/webUI/src/views/acc/App.vue @@ -217,6 +217,7 @@ export default { { path: '/acc/plugins/tax/settings', key: 'T', label: this.$t('drawer.tax_settings'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') }, { path: '/acc/plugins/custominvoice/templates', key: 'I', label: 'قالب‌های فاکتور', ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('custominvoice') }, { path: '/acc/plugins/import-workflow', key: 'I', label: 'مدیریت واردات کالا', ctrl: true, shift: true, permission: () => this.permissions.importWorkflow }, + { path: '/acc/storeroom/tickets/list/helper', key: 'I', label: this.$t('drawer.storeroom_ticket_helper'), ctrl: true, shift: true, permission: () => (this.permissions.storehelper || this.permissions.store) && this.isPluginActive('accpro') }, ]; }, restorePermissions(shortcuts) { @@ -555,7 +556,7 @@ export default { {{ $t('drawer.acc_store_tools') }} - + + + + {{ $t('drawer.storeroom_ticket_helper') }} + {{ getShortcutKey('/acc/storeroom/tickets/list/helper') + }} + + {{ $t('drawer.commodity_exist_count') }} diff --git a/webUI/src/views/acc/plugins/import-workflow/intro.vue b/webUI/src/views/acc/plugins/import-workflow/intro.vue index 4196921..69f2813 100644 --- a/webUI/src/views/acc/plugins/import-workflow/intro.vue +++ b/webUI/src/views/acc/plugins/import-workflow/intro.vue @@ -11,8 +11,8 @@

نسخه 1.0.0 - فعال - + فعال + خرید @@ -123,7 +123,7 @@