From ba9fe02ce782a4f11193f3f1a3c83c3665d34cdb Mon Sep 17 00:00:00 2001 From: Babak Alizadeh Date: Sat, 9 Aug 2025 16:24:45 +0000 Subject: [PATCH] addcustom invoice template plugin --- .../migrations/Version20250809100001.php | 30 + .../migrations/Version20250809103000.php | 25 + .../migrations/Version20250809112000.php | 43 ++ hesabixCore/src/Controller/BuyController.php | 138 +++- .../Controller/Plugins/PlugCustomInvoice.php | 476 ++++++++++++- .../src/Controller/PrintersController.php | 51 ++ .../src/Controller/RfbuyController.php | 95 ++- .../src/Controller/RfsellController.php | 101 ++- hesabixCore/src/Controller/SellController.php | 88 ++- .../src/Entity/CustomInvoiceTemplate.php | 112 +++ hesabixCore/src/Entity/PrintOptions.php | 60 ++ .../CustomInvoiceTemplateRepository.php | 18 + .../CustomInvoice/TemplateRenderer.php | 66 ++ .../templates/pdf/printers/sell.html.twig | 25 +- webUI/src/components/MonacoEditor.vue | 189 +++++- .../plugins/custominvoice/template-form.vue | 636 ++++++++++++------ .../acc/plugins/custominvoice/templates.vue | 508 ++++++++------ webUI/src/views/acc/settings/print.vue | 80 ++- 18 files changed, 2226 insertions(+), 515 deletions(-) create mode 100644 hesabixCore/migrations/Version20250809100001.php create mode 100644 hesabixCore/migrations/Version20250809103000.php create mode 100644 hesabixCore/migrations/Version20250809112000.php create mode 100644 hesabixCore/src/Entity/CustomInvoiceTemplate.php create mode 100644 hesabixCore/src/Repository/CustomInvoiceTemplateRepository.php create mode 100644 hesabixCore/src/Service/CustomInvoice/TemplateRenderer.php diff --git a/hesabixCore/migrations/Version20250809100001.php b/hesabixCore/migrations/Version20250809100001.php new file mode 100644 index 0000000..afc4739 --- /dev/null +++ b/hesabixCore/migrations/Version20250809100001.php @@ -0,0 +1,30 @@ +addSql('CREATE TABLE custom_invoice_template (id INT AUTO_INCREMENT NOT NULL, bid_id INT NOT NULL, submitter_id INT NOT NULL, name VARCHAR(255) NOT NULL, is_public TINYINT(1) NOT NULL, code LONGTEXT NOT NULL, INDEX IDX_CUSTOM_INV_TPL_BID (bid_id), INDEX IDX_CUSTOM_INV_TPL_SUBMITTER (submitter_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE custom_invoice_template ADD CONSTRAINT FK_CUSTOM_INV_TPL_BID FOREIGN KEY (bid_id) REFERENCES business (id)'); + $this->addSql('ALTER TABLE custom_invoice_template ADD CONSTRAINT FK_CUSTOM_INV_TPL_SUBMITTER FOREIGN KEY (submitter_id) REFERENCES `user` (id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE custom_invoice_template DROP FOREIGN KEY FK_CUSTOM_INV_TPL_BID'); + $this->addSql('ALTER TABLE custom_invoice_template DROP FOREIGN KEY FK_CUSTOM_INV_TPL_SUBMITTER'); + $this->addSql('DROP TABLE custom_invoice_template'); + } +} \ No newline at end of file diff --git a/hesabixCore/migrations/Version20250809103000.php b/hesabixCore/migrations/Version20250809103000.php new file mode 100644 index 0000000..03ddbc1 --- /dev/null +++ b/hesabixCore/migrations/Version20250809103000.php @@ -0,0 +1,25 @@ +addSql('ALTER TABLE custom_invoice_template ADD copy_count INT NOT NULL DEFAULT 0'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE custom_invoice_template DROP COLUMN copy_count'); + } +} \ No newline at end of file diff --git a/hesabixCore/migrations/Version20250809112000.php b/hesabixCore/migrations/Version20250809112000.php new file mode 100644 index 0000000..9a78f11 --- /dev/null +++ b/hesabixCore/migrations/Version20250809112000.php @@ -0,0 +1,43 @@ +addSql('ALTER TABLE print_options ADD sell_template_id INT DEFAULT NULL, ADD buy_template_id INT DEFAULT NULL, ADD rfbuy_template_id INT DEFAULT NULL, ADD rfsell_template_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE print_options ADD CONSTRAINT FK_PRINT_OPTIONS_SELL_TEMPLATE FOREIGN KEY (sell_template_id) REFERENCES custom_invoice_template (id) ON DELETE SET NULL'); + $this->addSql('ALTER TABLE print_options ADD CONSTRAINT FK_PRINT_OPTIONS_BUY_TEMPLATE FOREIGN KEY (buy_template_id) REFERENCES custom_invoice_template (id) ON DELETE SET NULL'); + $this->addSql('ALTER TABLE print_options ADD CONSTRAINT FK_PRINT_OPTIONS_RFBUY_TEMPLATE FOREIGN KEY (rfbuy_template_id) REFERENCES custom_invoice_template (id) ON DELETE SET NULL'); + $this->addSql('ALTER TABLE print_options ADD CONSTRAINT FK_PRINT_OPTIONS_RFSELL_TEMPLATE FOREIGN KEY (rfsell_template_id) REFERENCES custom_invoice_template (id) ON DELETE SET NULL'); + $this->addSql('CREATE INDEX IDX_PRINT_OPTIONS_SELL_TEMPLATE ON print_options (sell_template_id)'); + $this->addSql('CREATE INDEX IDX_PRINT_OPTIONS_BUY_TEMPLATE ON print_options (buy_template_id)'); + $this->addSql('CREATE INDEX IDX_PRINT_OPTIONS_RFBUY_TEMPLATE ON print_options (rfbuy_template_id)'); + $this->addSql('CREATE INDEX IDX_PRINT_OPTIONS_RFSELL_TEMPLATE ON print_options (rfsell_template_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE print_options DROP FOREIGN KEY FK_PRINT_OPTIONS_SELL_TEMPLATE'); + $this->addSql('ALTER TABLE print_options DROP FOREIGN KEY FK_PRINT_OPTIONS_BUY_TEMPLATE'); + $this->addSql('ALTER TABLE print_options DROP FOREIGN KEY FK_PRINT_OPTIONS_RFBUY_TEMPLATE'); + $this->addSql('ALTER TABLE print_options DROP FOREIGN KEY FK_PRINT_OPTIONS_RFSELL_TEMPLATE'); + $this->addSql('DROP INDEX IDX_PRINT_OPTIONS_SELL_TEMPLATE ON print_options'); + $this->addSql('DROP INDEX IDX_PRINT_OPTIONS_BUY_TEMPLATE ON print_options'); + $this->addSql('DROP INDEX IDX_PRINT_OPTIONS_RFBUY_TEMPLATE ON print_options'); + $this->addSql('DROP INDEX IDX_PRINT_OPTIONS_RFSELL_TEMPLATE ON print_options'); + $this->addSql('ALTER TABLE print_options DROP sell_template_id, DROP buy_template_id, DROP rfbuy_template_id, DROP rfsell_template_id'); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Controller/BuyController.php b/hesabixCore/src/Controller/BuyController.php index 9d24566..c678f0e 100644 --- a/hesabixCore/src/Controller/BuyController.php +++ b/hesabixCore/src/Controller/BuyController.php @@ -16,6 +16,8 @@ use App\Entity\Person; use App\Entity\PrintOptions; use App\Entity\StoreroomTicket; use App\Service\Printers; +use App\Entity\CustomInvoiceTemplate; +use App\Service\CustomInvoice\TemplateRenderer; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -424,7 +426,7 @@ class BuyController extends AbstractController } #[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): JsonResponse + public function app_buy_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, TemplateRenderer $renderer): JsonResponse { $params = []; if ($content = $request->getContent()) { @@ -456,53 +458,104 @@ class BuyController extends AbstractController } $pdfPid = 0; if ($params['pdf']) { - $printOptions = [ - 'bidInfo' => true, - 'pays' => true, - 'taxInfo' => true, - 'discountInfo' => true, - 'note' => true, - 'paper' => 'A4-L' - ]; - if (array_key_exists('printOptions', $params)) { - if (array_key_exists('bidInfo', $params['printOptions'])) { - $printOptions['bidInfo'] = $params['printOptions']['bidInfo']; - } - if (array_key_exists('pays', $params['printOptions'])) { - $printOptions['pays'] = $params['printOptions']['pays']; - } - if (array_key_exists('taxInfo', $params['printOptions'])) { - $printOptions['taxInfo'] = $params['printOptions']['taxInfo']; - } - if (array_key_exists('discountInfo', $params['printOptions'])) { - $printOptions['discountInfo'] = $params['printOptions']['discountInfo']; - } - if (array_key_exists('note', $params['printOptions'])) { - $printOptions['note'] = $params['printOptions']['note']; - } - if (array_key_exists('paper', $params['printOptions'])) { - $printOptions['paper'] = $params['printOptions']['paper']; - } - } - $note = ''; + // Build print options from defaults and overrides $printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid' => $acc['bid']]); + $defaultOptions = [ + 'bidInfo' => $printSettings ? $printSettings->isBuyBidInfo() : true, + 'pays' => $printSettings ? $printSettings->isBuyPays() : true, + 'taxInfo' => $printSettings ? $printSettings->isBuyTaxInfo() : true, + 'discountInfo' => $printSettings ? $printSettings->isBuyDiscountInfo() : true, + 'note' => $printSettings ? $printSettings->isBuyNote() : true, + 'paper' => $printSettings ? $printSettings->getBuyPaper() : 'A4-L', + ]; + $printOptions = array_merge($defaultOptions, $params['printOptions'] ?? []); + + $note = ''; if ($printSettings) { $note = $printSettings->getBuyNoteString(); } - $pdfPid = $provider->createPrint( - $acc['bid'], - $this->getUser(), - $this->renderView('pdf/printers/buy.html.twig', [ + + // Build safe context + $rowsArr = array_map(function ($row) { + return [ + 'commodity' => $row->getCommodity() ? [ + 'name' => method_exists($row->getCommodity(), 'getName') ? $row->getCommodity()->getName() : null, + 'code' => method_exists($row->getCommodity(), 'getCode') ? $row->getCommodity()->getCode() : null, + ] : null, + 'commodityCount' => $row->getCommdityCount(), + 'des' => $row->getDes(), + 'bs' => $row->getBs(), + 'tax' => $row->getTax(), + 'discount' => $row->getDiscount(), + ]; + }, $doc->getHesabdariRows()->toArray()); + + $personArr = $person ? [ + 'name' => $person->getName(), + 'mobile' => $person->getMobile(), + 'tel' => $person->getTel(), + 'address' => $person->getAddress(), + ] : null; + + $biz = $acc['bid']; + $businessArr = $biz ? [ + 'name' => method_exists($biz, 'getName') ? $biz->getName() : null, + 'tel' => method_exists($biz, 'getTel') ? $biz->getTel() : null, + 'mobile' => method_exists($biz, 'getMobile') ? $biz->getMobile() : null, + 'address' => method_exists($biz, 'getAddress') ? $biz->getAddress() : null, + 'shenasemeli' => method_exists($biz, 'getShenasemeli') ? $biz->getShenasemeli() : null, + 'codeeghtesadi' => method_exists($biz, 'getCodeeghtesadi') ? $biz->getCodeeghtesadi() : null, + ] : null; + + $context = [ + 'business' => $businessArr, + 'doc' => [ + 'code' => $doc->getCode(), + 'date' => method_exists($doc, 'getDate') ? $doc->getDate() : null, + ], + 'rows' => $rowsArr, + 'person' => $personArr, + 'discount' => $discount, + 'transfer' => $transfer, + 'printOptions' => $printOptions, + 'note' => $note, + ]; + + // Decide template: custom or default + $html = null; + $selectedTemplate = $printSettings ? $printSettings->getBuyTemplate() : null; + if ($selectedTemplate instanceof CustomInvoiceTemplate) { + $html = $renderer->render($selectedTemplate->getCode() ?? '', $context); + } + if ($html === null) { + $html = $this->renderView('pdf/printers/buy.html.twig', [ 'bid' => $acc['bid'], 'doc' => $doc, - 'rows' => $doc->getHesabdariRows(), + 'rows' => array_map(function ($row) { + return [ + 'commodity' => $row->getCommodity(), + 'commodityCount' => $row->getCommdityCount(), + 'commdityCount' => $row->getCommdityCount(), + 'des' => $row->getDes(), + 'bs' => $row->getBs(), + 'bd' => $row->getBd(), + 'tax' => $row->getTax(), + 'discount' => $row->getDiscount(), + ]; + }, $doc->getHesabdariRows()->toArray()), 'person' => $person, 'printInvoice' => $params['printers'], 'discount' => $discount, 'transfer' => $transfer, 'printOptions' => $printOptions, 'note' => $note - ]), + ]); + } + + $pdfPid = $provider->createPrint( + $acc['bid'], + $this->getUser(), + $html, false, $printOptions['paper'] ); @@ -514,7 +567,18 @@ class BuyController extends AbstractController $this->renderView('pdf/posPrinters/justBuy.html.twig', [ 'bid' => $acc['bid'], 'doc' => $doc, - 'rows' => $doc->getHesabdariRows(), + 'rows' => array_map(function ($row) { + return [ + 'commodity' => $row->getCommodity(), + 'commodityCount' => $row->getCommdityCount(), + 'commdityCount' => $row->getCommdityCount(), + 'des' => $row->getDes(), + 'bs' => $row->getBs(), + 'bd' => $row->getBd(), + 'tax' => $row->getTax(), + 'discount' => $row->getDiscount(), + ]; + }, $doc->getHesabdariRows()->toArray()), ]), false ); diff --git a/hesabixCore/src/Controller/Plugins/PlugCustomInvoice.php b/hesabixCore/src/Controller/Plugins/PlugCustomInvoice.php index 08d41e9..da12eec 100644 --- a/hesabixCore/src/Controller/Plugins/PlugCustomInvoice.php +++ b/hesabixCore/src/Controller/Plugins/PlugCustomInvoice.php @@ -7,16 +7,14 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Doctrine\ORM\EntityManagerInterface; -use App\Entity\PlugGhestaDoc; -use App\Entity\PlugGhestaItem; -use App\Entity\HesabdariDoc; -use App\Entity\Person; use App\Service\Access; -use App\Service\Provider; -use App\Service\Printers; -use App\Entity\PrintOptions; use App\Service\Log; use App\Entity\Business; +use App\Entity\CustomInvoiceTemplate; +use App\Entity\PrintOptions; +use App\Service\CustomInvoice\TemplateRenderer; +use App\Service\Provider; +use Throwable; class PlugCustomInvoice extends AbstractController { @@ -26,5 +24,467 @@ class PlugCustomInvoice extends AbstractController { $this->entityManager = $entityManager; } - + + #[Route('/api/plugins/custominvoice/template', name: 'plugins_custominvoice_template_create', methods: ['POST'])] + public function createTemplate(Request $request, Access $access, Log $log, TemplateRenderer $renderer): JsonResponse + { + $acc = $access->hasRole('settings'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $params = []; + if ($content = $request->getContent()) { + $params = json_decode($content, true) ?? []; + } + + $name = $params['name'] ?? null; + $isPublic = (bool)($params['isPublic'] ?? false); + $code = $params['code'] ?? null; + + if (!$name || !$code) { + return new JsonResponse(['status' => 'error', 'message' => 'name and code are required'], 400); + } + + // Validate template before saving + try { + $renderer->render($code, $this->buildSampleContext($acc['bid'])); + } catch (Throwable $e) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'خطا در قالب: ' . $e->getMessage(), + 'hint' => 'اگر قالب مشکل داشته باشد، در زمان چاپِ اسناد ممکن است هیچ خروجی تولید نشود.', + ], 400); + } + + $template = new CustomInvoiceTemplate(); + $template->setBid($acc['bid']); + $template->setSubmitter($this->getUser()); + $template->setName($name); + $template->setIsPublic($isPublic); + $template->setCode($code); + + $this->entityManager->persist($template); + $this->entityManager->flush(); + + $log->insert('قالب فاکتور سفارشی', 'ایجاد قالب: ' . $name, $this->getUser(), $acc['bid']); + + return $this->json([ + 'status' => 'ok', + 'data' => [ + 'id' => $template->getId(), + 'name' => $template->getName(), + 'isPublic' => $template->isPublic(), + 'code' => $template->getCode(), + ], + ]); + } + + #[Route('/api/plugins/custominvoice/template/{id<\d+>}', name: 'plugins_custominvoice_template_get', methods: ['GET'])] + public function getTemplate(int $id, Access $access): JsonResponse + { + $acc = $access->hasRole('settings'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $template = $this->entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([ + 'id' => $id, + 'bid' => $acc['bid'], + ]); + if (!$template) { + return new JsonResponse(['status' => 'error', 'message' => 'Template not found'], 404); + } + + return $this->json([ + 'status' => 'ok', + 'data' => [ + 'id' => $template->getId(), + 'name' => $template->getName(), + 'isPublic' => $template->isPublic(), + 'code' => $template->getCode(), + ], + ]); + } + + #[Route('/api/plugins/custominvoice/template/{id<\d+>}', name: 'plugins_custominvoice_template_update', methods: ['PUT','POST'])] + public function updateTemplate(int $id, Request $request, Access $access, Log $log, TemplateRenderer $renderer): JsonResponse + { + $acc = $access->hasRole('settings'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $template = $this->entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([ + 'id' => $id, + 'bid' => $acc['bid'], + ]); + if (!$template) { + return new JsonResponse(['status' => 'error', 'message' => 'Template not found'], 404); + } + + $params = []; + if ($content = $request->getContent()) { + $params = json_decode($content, true) ?? []; + } + + if (array_key_exists('name', $params)) { + $template->setName((string)$params['name']); + } + if (array_key_exists('isPublic', $params)) { + $template->setIsPublic((bool)$params['isPublic']); + } + if (array_key_exists('code', $params)) { + $newCode = (string)$params['code']; + try { + $renderer->render($newCode, $this->buildSampleContext($acc['bid'])); + } catch (Throwable $e) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'خطا در قالب: ' . $e->getMessage(), + 'hint' => 'اگر قالب مشکل داشته باشد، در زمان چاپِ اسناد ممکن است هیچ خروجی تولید نشود.', + ], 400); + } + $template->setCode($newCode); + } + + $this->entityManager->persist($template); + $this->entityManager->flush(); + + $log->insert('قالب فاکتور سفارشی', 'ویرایش قالب: ' . $template->getName(), $this->getUser(), $acc['bid']); + + return $this->json([ + 'status' => 'ok', + 'data' => [ + 'id' => $template->getId(), + 'name' => $template->getName(), + 'isPublic' => $template->isPublic(), + 'code' => $template->getCode(), + ], + ]); + } + + #[Route('/api/plugins/custominvoice/template/preview', name: 'plugins_custominvoice_template_preview', methods: ['POST'])] + public function preview(Request $request, Access $access, TemplateRenderer $renderer, Provider $provider): JsonResponse + { + $acc = $access->hasRole('settings'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $params = json_decode($request->getContent(), true) ?? []; + $code = (string)($params['code'] ?? ''); + $paper = (string)($params['paper'] ?? 'A4-L'); + if ($code === '') { + return new JsonResponse(['status' => 'error', 'message' => 'code is required'], 400); + } + + try { + $context = $this->buildSampleContext($acc['bid']); + $html = $renderer->render($code, $context); + } catch (Throwable $e) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'خطا در قالب: ' . $e->getMessage(), + ], 400); + } + + $pid = $provider->createPrint($acc['bid'], $this->getUser(), $html, false, $paper); + $printUrl = $this->generateUrl('app_front_print', ['id' => $pid]); + + return $this->json([ + 'status' => 'ok', + 'html' => $html, + 'printId' => $pid, + 'printUrl' => $printUrl, + 'warning' => 'این یک پیش فیمایش است. اگر قالب مشکل داشته باشد، هنگام چاپ اسناد ممکن است خروجی تولید نشود.', + ]); + } + + private function buildSampleContext(Business $bid): array + { + $now = time(); + return [ + 'accountStatus' => [ + 'value' => 1250000, + 'label' => 'بدهکار', + ], + 'bid' => [ + 'id' => method_exists($bid, 'getId') ? $bid->getId() : 0, + 'legalName' => 'شرکت نمونه', + 'shenasemeli' => '1234567890', + 'shomaresabt' => '987654', + 'codeeghtesadi' => '123456789012', + 'tel' => '021-12345678', + 'postalcode' => '1234567890', + 'ostan' => 'تهران', + 'shahrestan' => 'تهران', + 'address' => 'خیابان نمونه، کوچه یک، پلاک 1', + ], + 'business' => [ + 'name' => 'کسب 4 کار نمونه', + 'tel' => '021-12345678', + 'mobile' => '09120000000', + 'address' => 'تهران، ایران', + 'shenasemeli' => '1234567890', + 'codeeghtesadi' => '123456789012', + 'id' => method_exists($bid, 'getId') ? $bid->getId() : 0, + ], + 'doc' => [ + 'code' => 'INV-0001', + 'date' => date('Y/m/d', $now), + 'taxPercent' => 9, + 'discountPercent' => 5, + 'discountType' => 'amount', + 'amount' => 2350000, + 'relatedDocs' => [ + ['date' => date('Y/m/d', $now), 'amount' => 500000, 'des' => 'پرداخت شماره 1'], + ], + 'money' => [ + 'shortName' => 'ریال', + ], + ], + 'rows' => [ + [ + 'commodity' => [ + 'code' => 'P-001', + 'name' => 'کالای A', + 'unit' => ['name' => 'عدد'], + ], + 'commodityCount' => 2, + 'des' => 'شرح آیتم اول', + 'bs' => 1000000, + 'tax' => 90000, + 'discount' => 50000, + 'showPercentDiscount' => false, + 'discountPercent' => 0, + ], + [ + 'commodity' => [ + 'code' => 'S-002', + 'name' => 'خدمت B', + 'unit' => ['name' => 'ساعت'], + ], + 'commodityCount' => 3, + 'des' => 'شرح آیتم دوم', + 'bs' => 1350000, + 'tax' => 121500, + 'discount' => 0, + 'showPercentDiscount' => true, + 'discountPercent' => 10, + ], + ], + 'person' => [ + 'prelabel' => ['label' => 'جناب'], + 'nikename' => 'مشتری نمونه', + 'shenasemeli' => '1234567890', + 'sabt' => '112233', + 'codeeghtesadi' => '556677889900', + 'tel' => '021-88888888', + 'postalcode' => '1234567890', + 'ostan' => 'تهران', + 'shahr' => 'تهران', + 'address' => 'خیابان مشتری، پلاک 10', + ], + 'discount' => 50000, + 'transfer' => 20000, + 'printOptions' => [ + 'invoiceIndex' => true, + 'discountInfo' => true, + 'taxInfo' => true, + 'pays' => true, + 'note' => true, + 'businessStamp' => true, + 'paper' => 'A4-L', + ], + 'note' => 'یادداشت آزمایشی: این فقط پیش فیمایش است.', + ]; + } + + #[Route('/api/plugins/custominvoice/template/{id<\d+>}/copy', name: 'plugins_custominvoice_template_copy', methods: ['POST'])] + public function copyTemplate(int $id, Request $request, Access $access, Log $log): JsonResponse + { + $acc = $access->hasRole('settings'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $source = $this->entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([ + 'id' => $id, + ]); + if (!$source || !$source->isPublic()) { + return new JsonResponse(['status' => 'error', 'message' => 'Source template not found or not public'], 404); + } + + $new = new CustomInvoiceTemplate(); + $new->setBid($acc['bid']); + $new->setSubmitter($this->getUser()); + $new->setName($source->getName()); + $new->setIsPublic(false); + $new->setCode($source->getCode()); + + $this->entityManager->persist($new); + + // increment source copy count + $source->setCopyCount($source->getCopyCount() + 1); + $this->entityManager->persist($source); + + $this->entityManager->flush(); + + $log->insert('قالب فاکتور سفارشی', 'کپی از قالب عمومی: ' . $source->getName(), $this->getUser(), $acc['bid']); + + return $this->json([ + 'status' => 'ok', + 'data' => [ + 'id' => $new->getId(), + ] + ]); + } + + #[Route('/api/plugins/custominvoice/template/list', name: 'plugins_custominvoice_template_list', methods: ['POST'])] + public function listTemplates(Request $request, Access $access): JsonResponse + { + $acc = $access->hasRole('settings'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $params = json_decode($request->getContent(), true) ?? []; + + $page = max(1, (int)($params['page'] ?? 1)); + $limit = max(1, min(100, (int)($params['itemsPerPage'] ?? 10))); + $search = trim((string)($params['search'] ?? '')); + $sortBy = $params['sortBy'] ?? []; + $scope = strtolower((string)($params['scope'] ?? 'business')); + if (!in_array($scope, ['business', 'public'], true)) { + $scope = 'business'; + } + + $qb = $this->entityManager->getRepository(CustomInvoiceTemplate::class) + ->createQueryBuilder('t') + ->leftJoin('t.submitter', 'u') + ->addSelect('u'); + + if ($scope === 'business') { + $qb->andWhere('t.bid = :bid') + ->setParameter('bid', $acc['bid']); + } else { + $qb->andWhere('t.isPublic = 1'); + } + + if ($search !== '') { + $qb->andWhere( + $qb->expr()->orX( + 't.name LIKE :search', + 't.code LIKE :search', + 'u.fullName LIKE :search', + 'u.email LIKE :search' + ) + )->setParameter('search', '%' . $search . '%'); + } + + if (is_array($sortBy) && count($sortBy) > 0) { + $firstSort = $sortBy[0]; + $key = in_array($firstSort['key'] ?? 'id', ['id', 'name', 'isPublic', 'copyCount']) ? $firstSort['key'] : 'id'; + $order = strtoupper($firstSort['order'] ?? 'DESC'); + $order = $order === 'ASC' ? 'ASC' : 'DESC'; + $qb->orderBy('t.' . $key, $order); + } else { + // default: for public scope, popular first; else by id desc + if ($scope === 'public') { + $qb->orderBy('t.copyCount', 'DESC')->addOrderBy('t.id', 'DESC'); + } else { + $qb->orderBy('t.id', 'DESC'); + } + } + + $total = (int)(clone $qb)->select('COUNT(t.id)')->getQuery()->getSingleScalarResult(); + + $items = $qb->setFirstResult(($page - 1) * $limit) + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + + $currentBidId = method_exists($acc['bid'], 'getId') ? $acc['bid']->getId() : null; + + $data = array_map(function (CustomInvoiceTemplate $t) use ($currentBidId) { + $ownedByMe = $currentBidId !== null && $t->getBid() && $t->getBid()->getId() === $currentBidId; + return [ + 'id' => $t->getId(), + 'name' => $t->getName(), + 'isPublic' => $t->isPublic(), + 'code' => $t->getCode(), + 'ownedByMe' => $ownedByMe, + 'submitter' => $t->getSubmitter() ? [ + 'id' => $t->getSubmitter()->getId(), + 'fullName' => $t->getSubmitter()->getFullName(), + 'email' => $t->getSubmitter()->getEmail(), + ] : null, + 'copyCount' => $t->getCopyCount(), + 'popular' => $t->getCopyCount() > 10, + ]; + }, $items); + + return $this->json([ + 'items' => $data, + 'total' => $total, + 'page' => $page, + 'itemsPerPage' => $limit, + ]); + } + + #[Route('/api/plugins/custominvoice/template/{id<\d+>}', name: 'plugins_custominvoice_template_delete', methods: ['DELETE'])] + public function deleteTemplate(int $id, Access $access, Log $log): JsonResponse + { + $acc = $access->hasRole('settings'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $template = $this->entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([ + 'id' => $id, + 'bid' => $acc['bid'], + ]); + + if (!$template) { + return new JsonResponse(['status' => 'error', 'message' => 'Template not found'], 404); + } + + // Before delete, nullify references in PrintOptions for this business + $printOptionsList = $this->entityManager->getRepository(PrintOptions::class)->findBy([ + 'bid' => $acc['bid'], + ]); + foreach ($printOptionsList as $opt) { + $changed = false; + if ($opt->getSellTemplate() && $opt->getSellTemplate()->getId() === $template->getId()) { + $opt->setSellTemplate(null); + $changed = true; + } + if ($opt->getBuyTemplate() && $opt->getBuyTemplate()->getId() === $template->getId()) { + $opt->setBuyTemplate(null); + $changed = true; + } + if ($opt->getRfbuyTemplate() && $opt->getRfbuyTemplate()->getId() === $template->getId()) { + $opt->setRfbuyTemplate(null); + $changed = true; + } + if ($opt->getRfsellTemplate() && $opt->getRfsellTemplate()->getId() === $template->getId()) { + $opt->setRfsellTemplate(null); + $changed = true; + } + if ($changed) { + $this->entityManager->persist($opt); + } + } + // Apply changes before deleting the template to avoid FK issues when migrations not applied + $this->entityManager->flush(); + + $name = $template->getName(); + $this->entityManager->remove($template); + $this->entityManager->flush(); + + $log->insert('قالب فاکتور سفارشی', 'حذف قالب: ' . $name, $this->getUser(), $acc['bid']); + + return $this->json(['status' => 'ok']); + } } \ No newline at end of file diff --git a/hesabixCore/src/Controller/PrintersController.php b/hesabixCore/src/Controller/PrintersController.php index c5225e4..6705ebe 100644 --- a/hesabixCore/src/Controller/PrintersController.php +++ b/hesabixCore/src/Controller/PrintersController.php @@ -6,6 +6,7 @@ use App\Entity\Printer; use App\Entity\PrinterQueue; use App\Entity\PrintItem; use App\Entity\PrintOptions; +use App\Entity\CustomInvoiceTemplate; use App\Service\Access; use App\Service\Explore; use App\Service\Extractor; @@ -59,6 +60,7 @@ class PrintersController extends AbstractController $temp['sell']['paper'] = $settings->getSellPaper(); $temp['sell']['businessStamp'] = $settings->isSellBusinessStamp(); $temp['sell']['invoiceIndex'] = $settings->isSellInvoiceIndex(); + $temp['sell']['templateId'] = $settings->getSellTemplate() ? $settings->getSellTemplate()->getId() : null; if (!$temp['sell']['paper']) { $temp['sell']['paper'] = 'A4-L'; } @@ -71,6 +73,7 @@ class PrintersController extends AbstractController $temp['buy']['noteString'] = $settings->getBuyNoteString(); $temp['buy']['pays'] = $settings->isBuyPays(); $temp['buy']['paper'] = $settings->getBuyPaper(); + $temp['buy']['templateId'] = $settings->getBuyTemplate() ? $settings->getBuyTemplate()->getId() : null; if (!$temp['buy']['paper']) { $temp['buy']['paper'] = 'A4-L'; } @@ -83,6 +86,7 @@ class PrintersController extends AbstractController $temp['rfbuy']['noteString'] = $settings->getRfbuyNoteString(); $temp['rfbuy']['pays'] = $settings->isRfbuyPays(); $temp['rfbuy']['paper'] = $settings->getRfbuyPaper(); + $temp['rfbuy']['templateId'] = $settings->getRfbuyTemplate() ? $settings->getRfbuyTemplate()->getId() : null; if (!$temp['rfbuy']['paper']) { $temp['rfbuy']['paper'] = 'A4-L'; } @@ -95,6 +99,7 @@ class PrintersController extends AbstractController $temp['rfsell']['noteString'] = $settings->getRfsellNoteString(); $temp['rfsell']['pays'] = $settings->isRfsellPays(); $temp['rfsell']['paper'] = $settings->getRfsellPaper(); + $temp['rfsell']['templateId'] = $settings->getRfsellTemplate() ? $settings->getRfsellTemplate()->getId() : null; $temp['fastsell']['cashdeskTicket'] = $settings->isFastsellCashdeskTicket(); $temp['fastsell']['invoice'] = $settings->isFastsellInvoice(); @@ -139,6 +144,19 @@ class PrintersController extends AbstractController $settings->setSellPaper($params['sell']['paper']); $settings->setSellBusinessStamp($params['sell']['businessStamp'] ?? false); $settings->setSellInvoiceIndex($params['sell']['invoiceIndex'] ?? false); + + // Resolve templates by ID and ownership; update only if key exists + if (array_key_exists('sell', $params) && array_key_exists('templateId', $params['sell'])) { + $sellT = null; + if ($params['sell']['templateId']) { + $sellT = $entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([ + 'id' => (int)$params['sell']['templateId'], + 'bid' => $acc['bid'], + ]); + } + $settings->setSellTemplate($sellT); + } + if ($params['buy']['bidInfo'] == null) { $settings->setBuyBidInfo(false); } else { @@ -152,6 +170,17 @@ class PrintersController extends AbstractController $settings->setBuyPays($params['buy']['pays'] ?? false); $settings->setBuyPaper($params['buy']['paper']); + if (array_key_exists('buy', $params) && array_key_exists('templateId', $params['buy'])) { + $buyT = null; + if ($params['buy']['templateId']) { + $buyT = $entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([ + 'id' => (int)$params['buy']['templateId'], + 'bid' => $acc['bid'], + ]); + } + $settings->setBuyTemplate($buyT); + } + $settings->setRfbuyBidInfo($params['rfbuy']['bidInfo'] ?? false); $settings->setRfbuyTaxInfo($params['rfbuy']['taxInfo'] ?? false); $settings->setRfbuyDiscountInfo($params['rfbuy']['discountInfo'] ?? false); @@ -160,6 +189,17 @@ class PrintersController extends AbstractController $settings->setRfbuyPays($params['rfbuy']['pays'] ?? false); $settings->setRfbuyPaper($params['rfbuy']['paper']); + if (array_key_exists('rfbuy', $params) && array_key_exists('templateId', $params['rfbuy'])) { + $rfbuyT = null; + if ($params['rfbuy']['templateId']) { + $rfbuyT = $entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([ + 'id' => (int)$params['rfbuy']['templateId'], + 'bid' => $acc['bid'], + ]); + } + $settings->setRfbuyTemplate($rfbuyT); + } + $settings->setRfsellBidInfo($params['rfsell']['bidInfo'] ?? false); $settings->setRfsellTaxInfo($params['rfsell']['taxInfo'] ?? false); $settings->setRfsellDiscountInfo($params['rfsell']['discountInfo'] ?? false); @@ -168,6 +208,17 @@ class PrintersController extends AbstractController $settings->setRfsellPays($params['rfsell']['pays'] ?? false); $settings->setRfSellPaper($params['rfsell']['paper']); + if (array_key_exists('rfsell', $params) && array_key_exists('templateId', $params['rfsell'])) { + $rfsellT = null; + if ($params['rfsell']['templateId']) { + $rfsellT = $entityManager->getRepository(CustomInvoiceTemplate::class)->findOneBy([ + 'id' => (int)$params['rfsell']['templateId'], + 'bid' => $acc['bid'], + ]); + } + $settings->setRfsellTemplate($rfsellT); + } + $settings->setRepserviceNoteString($params['repservice']['noteString']); $settings->setRepServicePaper($params['repservice']['paper']); diff --git a/hesabixCore/src/Controller/RfbuyController.php b/hesabixCore/src/Controller/RfbuyController.php index 4269823..d699e85 100644 --- a/hesabixCore/src/Controller/RfbuyController.php +++ b/hesabixCore/src/Controller/RfbuyController.php @@ -14,6 +14,8 @@ use App\Entity\HesabdariTable; use App\Entity\InvoiceType; use App\Entity\Person; use App\Entity\PrintOptions; +use App\Entity\CustomInvoiceTemplate; +use App\Service\CustomInvoice\TemplateRenderer; use App\Entity\StoreroomTicket; use App\Service\Printers; use Doctrine\ORM\EntityManagerInterface; @@ -386,7 +388,7 @@ class RfbuyController extends AbstractController } #[Route('/api/rfbuy/print/invoice', name: 'app_rfbuy_print_invoice')] - public function app_rfbuy_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse + public function app_rfbuy_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, TemplateRenderer $renderer): JsonResponse { $params = []; if ($content = $request->getContent()) { @@ -447,20 +449,86 @@ class RfbuyController extends AbstractController $note = ''; $printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid'=>$acc['bid']]); if($printSettings){$note = $printSettings->getRfbuyNoteString();} - $pdfPid = $provider->createPrint( - $acc['bid'], - $this->getUser(), - $this->renderView('pdf/printers/rfbuy.html.twig', [ + // Build safe context + $rowsArr = array_map(function ($row) { + return [ + 'commodity' => $row->getCommodity() ? [ + 'name' => method_exists($row->getCommodity(), 'getName') ? $row->getCommodity()->getName() : null, + 'code' => method_exists($row->getCommodity(), 'getCode') ? $row->getCommodity()->getCode() : null, + ] : null, + 'commodityCount' => $row->getCommdityCount(), + 'des' => $row->getDes(), + 'bs' => $row->getBs(), + 'tax' => $row->getTax(), + 'discount' => $row->getDiscount(), + ]; + }, $doc->getHesabdariRows()->toArray()); + + $personArr = $person ? [ + 'name' => $person->getName(), + 'mobile' => $person->getMobile(), + 'tel' => $person->getTel(), + 'address' => $person->getAddress(), + ] : null; + + $biz = $acc['bid']; + $businessArr = $biz ? [ + 'name' => method_exists($biz, 'getName') ? $biz->getName() : null, + 'tel' => method_exists($biz, 'getTel') ? $biz->getTel() : null, + 'mobile' => method_exists($biz, 'getMobile') ? $biz->getMobile() : null, + 'address' => method_exists($biz, 'getAddress') ? $biz->getAddress() : null, + 'shenasemeli' => method_exists($biz, 'getShenasemeli') ? $biz->getShenasemeli() : null, + 'codeeghtesadi' => method_exists($biz, 'getCodeeghtesadi') ? $biz->getCodeeghtesadi() : null, + ] : null; + + $context = [ + 'business' => $businessArr, + 'doc' => [ + 'code' => $doc->getCode(), + 'date' => method_exists($doc, 'getDate') ? $doc->getDate() : null, + ], + 'rows' => $rowsArr, + 'person' => $personArr, + 'discount' => $discount, + 'transfer' => $transfer, + 'printOptions'=> $printOptions, + 'note'=> $note + ]; + + $html = null; + $selectedTemplate = $printSettings ? $printSettings->getRfbuyTemplate() : null; + if ($selectedTemplate instanceof CustomInvoiceTemplate) { + $html = $renderer->render($selectedTemplate->getCode() ?? '', $context); + } + if ($html === null) { + $html = $this->renderView('pdf/printers/rfbuy.html.twig', [ 'bid' => $acc['bid'], 'doc' => $doc, - 'rows' => $doc->getHesabdariRows(), + 'rows' => array_map(function ($row) { + return [ + 'commodity' => $row->getCommodity(), + 'commodityCount' => $row->getCommdityCount(), + 'commdityCount' => $row->getCommdityCount(), + 'des' => $row->getDes(), + 'bs' => $row->getBs(), + 'bd' => $row->getBd(), + 'tax' => $row->getTax(), + 'discount' => $row->getDiscount(), + ]; + }, $doc->getHesabdariRows()->toArray()), 'person' => $person, 'printInvoice' => $params['printers'], 'discount' => $discount, 'transfer' => $transfer, 'printOptions'=> $printOptions, 'note'=> $note - ]), + ]); + } + + $pdfPid = $provider->createPrint( + $acc['bid'], + $this->getUser(), + $html, false, $printOptions['paper'] ); @@ -472,7 +540,18 @@ class RfbuyController extends AbstractController $this->renderView('pdf/posPrinters/justRfbuy.html.twig', [ 'bid' => $acc['bid'], 'doc' => $doc, - 'rows' => $doc->getHesabdariRows(), + 'rows' => array_map(function ($row) { + return [ + 'commodity' => $row->getCommodity(), + 'commodityCount' => $row->getCommdityCount(), + 'commdityCount' => $row->getCommdityCount(), + 'des' => $row->getDes(), + 'bs' => $row->getBs(), + 'bd' => $row->getBd(), + 'tax' => $row->getTax(), + 'discount' => $row->getDiscount(), + ]; + }, $doc->getHesabdariRows()->toArray()), ]), false ); diff --git a/hesabixCore/src/Controller/RfsellController.php b/hesabixCore/src/Controller/RfsellController.php index 4f281ca..dade173 100644 --- a/hesabixCore/src/Controller/RfsellController.php +++ b/hesabixCore/src/Controller/RfsellController.php @@ -14,6 +14,8 @@ use App\Entity\HesabdariTable; use App\Entity\InvoiceType; use App\Entity\Person; use App\Entity\PrintOptions; +use App\Entity\CustomInvoiceTemplate; +use App\Service\CustomInvoice\TemplateRenderer; use App\Entity\StoreroomTicket; use App\Service\Printers; use Doctrine\ORM\EntityManagerInterface; @@ -408,7 +410,7 @@ class RfsellController extends AbstractController } #[Route('/api/rfsell/print/invoice', name: 'app_rfsell_print_invoice')] - public function app_rfsell_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse + public function app_rfsell_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, TemplateRenderer $renderer): JsonResponse { $params = []; if ($content = $request->getContent()) { @@ -469,20 +471,87 @@ class RfsellController extends AbstractController $note = ''; $printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid'=>$acc['bid']]); if($printSettings){$note = $printSettings->getRfsellNoteString();} - $pdfPid = $provider->createPrint( - $acc['bid'], - $this->getUser(), - $this->renderView('pdf/printers/rfsell.html.twig', [ + // Build safe context + $rowsArr = array_map(function ($row) { + return [ + 'commodity' => $row->getCommodity() ? [ + 'name' => method_exists($row->getCommodity(), 'getName') ? $row->getCommodity()->getName() : null, + 'code' => method_exists($row->getCommodity(), 'getCode') ? $row->getCommodity()->getCode() : null, + ] : null, + 'commodityCount' => $row->getCommdityCount(), + 'des' => $row->getDes(), + 'bs' => $row->getBs(), + 'tax' => $row->getTax(), + 'discount' => $row->getDiscount(), + ]; + }, $doc->getHesabdariRows()->toArray()); + + $personArr = $person ? [ + 'name' => $person->getName(), + 'mobile' => $person->getMobile(), + 'tel' => $person->getTel(), + 'address' => $person->getAddress(), + ] : null; + + $biz = $acc['bid']; + $businessArr = $biz ? [ + 'name' => method_exists($biz, 'getName') ? $biz->getName() : null, + 'tel' => method_exists($biz, 'getTel') ? $biz->getTel() : null, + 'mobile' => method_exists($biz, 'getMobile') ? $biz->getMobile() : null, + 'address' => method_exists($biz, 'getAddress') ? $biz->getAddress() : null, + 'shenasemeli' => method_exists($biz, 'getShenasemeli') ? $biz->getShenasemeli() : null, + 'codeeghtesadi' => method_exists($biz, 'getCodeeghtesadi') ? $biz->getCodeeghtesadi() : null, + ] : null; + + $context = [ + 'business' => $businessArr, + 'doc' => [ + 'code' => $doc->getCode(), + 'date' => method_exists($doc, 'getDate') ? $doc->getDate() : null, + ], + 'rows' => $rowsArr, + 'person' => $personArr, + 'discount' => $discount, + 'transfer' => $transfer, + 'printOptions'=> $printOptions, + 'note'=> $note + ]; + + $html = null; + $selectedTemplate = $printSettings ? $printSettings->getRfsellTemplate() : null; + if ($selectedTemplate instanceof CustomInvoiceTemplate) { + $html = $renderer->render($selectedTemplate->getCode() ?? '', $context); + } + if ($html === null) { + $html = $this->renderView('pdf/printers/rfsell.html.twig', [ 'bid' => $acc['bid'], 'doc' => $doc, - 'rows' => $doc->getHesabdariRows(), + 'rows' => array_map(function ($row) { + return [ + 'commodity' => $row->getCommodity(), + 'commodityCount' => $row->getCommdityCount(), + 'commdityCount' => $row->getCommdityCount(), + 'des' => $row->getDes(), + 'bs' => $row->getBs(), + 'tax' => $row->getTax(), + 'discount' => $row->getDiscount(), + 'showPercentDiscount' => $row->getDiscountType() === 'percent', + 'discountPercent' => $row->getDiscountPercent(), + ]; + }, $doc->getHesabdariRows()->toArray()), 'person' => $person, 'printInvoice' => $params['printers'], 'discount' => $discount, 'transfer' => $transfer, 'printOptions'=> $printOptions, 'note'=> $note - ]), + ]); + } + + $pdfPid = $provider->createPrint( + $acc['bid'], + $this->getUser(), + $html, false, $printOptions['paper'] ); @@ -494,8 +563,22 @@ class RfsellController extends AbstractController $this->renderView('pdf/posPrinters/justSell.html.twig', [ 'bid' => $acc['bid'], 'doc' => $doc, - 'rows' => $doc->getHesabdariRows(), - ]), + 'rows' => array_map(function ($row) { + return [ + 'commodity' => $row->getCommodity(), + 'commodityCount' => $row->getCommdityCount(), + 'commdityCount' => $row->getCommdityCount(), + 'des' => $row->getDes(), + 'bs' => $row->getBs(), + 'tax' => $row->getTax(), + 'discount' => $row->getDiscount(), + 'showPercentDiscount' => $row->getDiscountType() === 'percent', + 'discountPercent' => $row->getDiscountPercent(), + ]; + }, $doc->getHesabdariRows()->toArray()), + 'discount' => $discount, + 'transfer' => $transfer, + ]), false ); $printers->addFile($pid, $acc, "fastSellInvoice"); diff --git a/hesabixCore/src/Controller/SellController.php b/hesabixCore/src/Controller/SellController.php index 65a6f1d..83a8045 100644 --- a/hesabixCore/src/Controller/SellController.php +++ b/hesabixCore/src/Controller/SellController.php @@ -8,7 +8,6 @@ use App\Service\Log; use App\Service\Access; use App\Service\Explore; use App\Entity\Commodity; -use App\Service\PluginService; use App\Service\Provider; use App\Service\Extractor; use App\Entity\HesabdariDoc; @@ -31,6 +30,9 @@ use App\Entity\BankAccount; use App\Entity\Cashdesk; use App\Entity\Salary; use App\Entity\Year; +use App\Entity\CustomInvoiceTemplate; +use App\Service\CustomInvoice\TemplateRenderer; +use App\Service\PluginService; class SellController extends AbstractController { @@ -710,7 +712,7 @@ class SellController extends AbstractController } #[Route('/api/sell/print/invoice', name: 'app_sell_print_invoice')] - public function app_sell_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse + public function app_sell_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, PluginService $pluginService, TemplateRenderer $renderer): JsonResponse { $acc = $access->hasRole('sell'); if (!$acc) @@ -781,10 +783,76 @@ class SellController extends AbstractController if ($printSettings) { $note = $printSettings->getSellNoteString(); } - $pdfPid = $provider->createPrint( - $acc['bid'], - $this->getUser(), - $this->renderView('pdf/printers/sell.html.twig', [ + + // Build safe context data for rendering + $rowsArr = array_map(function ($row) { + return [ + 'commodity' => $row->getCommodity() ? [ + 'name' => method_exists($row->getCommodity(), 'getName') ? $row->getCommodity()->getName() : null, + 'code' => method_exists($row->getCommodity(), 'getCode') ? $row->getCommodity()->getCode() : null, + ] : null, + 'commodityCount' => $row->getCommdityCount(), + 'des' => $row->getDes(), + 'bs' => $row->getBs(), + 'tax' => $row->getTax(), + 'discount' => $row->getDiscount(), + 'showPercentDiscount' => $row->getDiscountType() === 'percent', + 'discountPercent' => $row->getDiscountPercent() + ]; + }, $doc->getHesabdariRows()->toArray()); + + $personArr = $person ? [ + 'name' => $person->getName(), + 'mobile' => $person->getMobile(), + 'tel' => $person->getTel(), + 'address' => $person->getAddress(), + ] : null; + + $biz = $acc['bid']; + $businessArr = $biz ? [ + 'name' => method_exists($biz, 'getName') ? $biz->getName() : null, + 'tel' => method_exists($biz, 'getTel') ? $biz->getTel() : null, + 'mobile' => method_exists($biz, 'getMobile') ? $biz->getMobile() : null, + 'address' => method_exists($biz, 'getAddress') ? $biz->getAddress() : null, + 'shenasemeli' => method_exists($biz, 'getShenasemeli') ? $biz->getShenasemeli() : null, + 'codeeghtesadi' => method_exists($biz, 'getCodeeghtesadi') ? $biz->getCodeeghtesadi() : null, + 'id' => method_exists($biz, 'getId') ? $biz->getId() : null, + ] : null; + + $context = [ + 'accountStatus' => $accountStatus, + 'business' => $businessArr, + 'bid' => $businessArr, + 'doc' => [ + 'code' => $doc->getCode(), + 'date' => method_exists($doc, 'getDate') ? $doc->getDate() : null, + 'taxPercent' => method_exists($doc, 'getTaxPercent') ? $doc->getTaxPercent() : null, + 'discountPercent' => $doc->getDiscountPercent(), + 'discountType' => $doc->getDiscountType(), + 'money' => [ + 'shortName' => method_exists($doc, 'getMoney') && $doc->getMoney() && method_exists($doc->getMoney(), 'getShortName') ? $doc->getMoney()->getShortName() : null, + ], + ], + 'rows' => $rowsArr, + 'person' => $personArr, + 'discount' => $discount, + 'transfer' => $transfer, + 'printOptions' => $printOptions, + 'note' => $note, + ]; + + // Decide template: custom or default + $html = null; + $isCustomInvoiceActive = $pluginService->isActive('custominvoice', $acc['bid']); + $selectedTemplate = $printSettings ? $printSettings->getSellTemplate() : null; + + if ($isCustomInvoiceActive && $selectedTemplate instanceof CustomInvoiceTemplate) { + $html = $renderer->render($selectedTemplate->getCode() ?? '', $context); + } + + if ($html === null) { + // fallback to default Twig template + $html = $this->renderView('pdf/printers/sell.html.twig', [ 'accountStatus' => $accountStatus, 'bid' => $acc['bid'], 'doc' => $doc, @@ -808,7 +876,13 @@ class SellController extends AbstractController 'note' => $note, 'showPercentDiscount' => $doc->getDiscountType() === 'percent', 'discountPercent' => $doc->getDiscountPercent() - ]), + ]); + } + + $pdfPid = $provider->createPrint( + $acc['bid'], + $this->getUser(), + $html, false, $printOptions['paper'] ); diff --git a/hesabixCore/src/Entity/CustomInvoiceTemplate.php b/hesabixCore/src/Entity/CustomInvoiceTemplate.php new file mode 100644 index 0000000..5ebe0ed --- /dev/null +++ b/hesabixCore/src/Entity/CustomInvoiceTemplate.php @@ -0,0 +1,112 @@ +id; + } + + public function getBid(): ?Business + { + return $this->bid; + } + + public function setBid(?Business $bid): static + { + $this->bid = $bid; + + return $this; + } + + public function getSubmitter(): ?User + { + return $this->submitter; + } + + public function setSubmitter(?User $submitter): static + { + $this->submitter = $submitter; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function isPublic(): bool + { + return $this->isPublic; + } + + public function setIsPublic(bool $isPublic): static + { + $this->isPublic = $isPublic; + + return $this; + } + + public function getCode(): ?string + { + return $this->code; + } + + public function setCode(string $code): static + { + $this->code = $code; + + return $this; + } + + public function getCopyCount(): int + { + return $this->copyCount; + } + + public function setCopyCount(int $copyCount): static + { + $this->copyCount = $copyCount; + return $this; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/PrintOptions.php b/hesabixCore/src/Entity/PrintOptions.php index 826c108..be80fcf 100644 --- a/hesabixCore/src/Entity/PrintOptions.php +++ b/hesabixCore/src/Entity/PrintOptions.php @@ -132,6 +132,22 @@ class PrintOptions #[ORM\Column(nullable: true)] private ?bool $sellBusinessStamp = null; + #[ORM\ManyToOne(targetEntity: CustomInvoiceTemplate::class)] + #[ORM\JoinColumn(name: 'sell_template_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + private ?CustomInvoiceTemplate $sellTemplate = null; + + #[ORM\ManyToOne(targetEntity: CustomInvoiceTemplate::class)] + #[ORM\JoinColumn(name: 'buy_template_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + private ?CustomInvoiceTemplate $buyTemplate = null; + + #[ORM\ManyToOne(targetEntity: CustomInvoiceTemplate::class)] + #[ORM\JoinColumn(name: 'rfbuy_template_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + private ?CustomInvoiceTemplate $rfbuyTemplate = null; + + #[ORM\ManyToOne(targetEntity: CustomInvoiceTemplate::class)] + #[ORM\JoinColumn(name: 'rfsell_template_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + private ?CustomInvoiceTemplate $rfsellTemplate = null; + public function getId(): ?int { return $this->id; @@ -602,4 +618,48 @@ class PrintOptions $this->sellBusinessStamp = $sellBusinessStamp; return $this; } + + public function getSellTemplate(): ?CustomInvoiceTemplate + { + return $this->sellTemplate; + } + + public function setSellTemplate(?CustomInvoiceTemplate $sellTemplate): self + { + $this->sellTemplate = $sellTemplate; + return $this; + } + + public function getBuyTemplate(): ?CustomInvoiceTemplate + { + return $this->buyTemplate; + } + + public function setBuyTemplate(?CustomInvoiceTemplate $buyTemplate): self + { + $this->buyTemplate = $buyTemplate; + return $this; + } + + public function getRfbuyTemplate(): ?CustomInvoiceTemplate + { + return $this->rfbuyTemplate; + } + + public function setRfbuyTemplate(?CustomInvoiceTemplate $rfbuyTemplate): self + { + $this->rfbuyTemplate = $rfbuyTemplate; + return $this; + } + + public function getRfsellTemplate(): ?CustomInvoiceTemplate + { + return $this->rfsellTemplate; + } + + public function setRfsellTemplate(?CustomInvoiceTemplate $rfsellTemplate): self + { + $this->rfsellTemplate = $rfsellTemplate; + return $this; + } } diff --git a/hesabixCore/src/Repository/CustomInvoiceTemplateRepository.php b/hesabixCore/src/Repository/CustomInvoiceTemplateRepository.php new file mode 100644 index 0000000..2a919d0 --- /dev/null +++ b/hesabixCore/src/Repository/CustomInvoiceTemplateRepository.php @@ -0,0 +1,18 @@ + + */ +class CustomInvoiceTemplateRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, CustomInvoiceTemplate::class); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/CustomInvoice/TemplateRenderer.php b/hesabixCore/src/Service/CustomInvoice/TemplateRenderer.php new file mode 100644 index 0000000..dd11717 --- /dev/null +++ b/hesabixCore/src/Service/CustomInvoice/TemplateRenderer.php @@ -0,0 +1,66 @@ +urlGenerator = $urlGenerator; + } + + /** + * Render user-provided template code with sandboxed Twig and a safe context. + * Only a small set of tags and filters are allowed; no functions, properties or methods. + */ + public function render(string $code, array $context = []): string + { + $loader = new ArrayLoader(['tpl' => $code]); + $twig = new Environment($loader, [ + 'autoescape' => 'html', + 'cache' => false, + 'strict_variables' => false, + ]); + + // Allow generating routes inside templates + $twig->addExtension(new RoutingExtension($this->urlGenerator)); + + $allowedTags = ['if', 'for', 'set']; + $allowedFilters = ['escape', 'upper', 'lower', 'number_format', 'join', 'round', 'length', 'raw']; + $allowedMethods = []; + $allowedProperties = []; + $allowedFunctions = ['path', 'url']; + $policy = new SecurityPolicy($allowedTags, $allowedFilters, $allowedMethods, $allowedProperties, $allowedFunctions); + $twig->addExtension(new SandboxExtension($policy, true)); + + // Ensure all objects are converted to arrays to avoid method/property access + $safeContext = $this->deepNormalize($context); + + return $twig->render('tpl', $safeContext); + } + + private function deepNormalize(mixed $value): mixed + { + if (is_array($value)) { + $normalized = []; + foreach ($value as $k => $v) { + $normalized[$k] = $this->deepNormalize($v); + } + return $normalized; + } + if (is_object($value)) { + // Expose nothing from raw objects + return []; + } + return $value; + } +} \ No newline at end of file diff --git a/hesabixCore/templates/pdf/printers/sell.html.twig b/hesabixCore/templates/pdf/printers/sell.html.twig index c3c12ab..6b2d8a6 100644 --- a/hesabixCore/templates/pdf/printers/sell.html.twig +++ b/hesabixCore/templates/pdf/printers/sell.html.twig @@ -4,27 +4,27 @@ @@ -208,7 +208,7 @@ {% set rowIndex = 0 %} {% for item in rows%} {% if item.commodity %} - {% set taxAll = taxAll + item.tax %} + {% set taxAll = taxAll + item.tax %} {% set rowIndex = rowIndex + 1 %} {{rowIndex}} @@ -304,16 +304,16 @@ {% endif %} - {# فیلد جدید وضعیت حساب مشتری #} + { # فیلد جدید وضعیت حساب مشتری #} {% if accountStatus is defined %}

وضعیت حساب مشتری با احتساب این فاکتور: {{ accountStatus.value | number_format}} {{ doc.money.shortName }} {{ accountStatus.label }} - +

- + {% endif %} @@ -371,4 +371,3 @@ - diff --git a/webUI/src/components/MonacoEditor.vue b/webUI/src/components/MonacoEditor.vue index 474244a..fbc5808 100644 --- a/webUI/src/components/MonacoEditor.vue +++ b/webUI/src/components/MonacoEditor.vue @@ -5,30 +5,174 @@ \ No newline at end of file diff --git a/webUI/src/views/acc/settings/print.vue b/webUI/src/views/acc/settings/print.vue index eb1470e..4dfabf0 100755 --- a/webUI/src/views/acc/settings/print.vue +++ b/webUI/src/views/acc/settings/print.vue @@ -65,6 +65,19 @@ + + + + + @@ -106,6 +119,19 @@ + + + + + @@ -141,6 +167,19 @@ + + + + + @@ -176,6 +215,19 @@ + + + + + @@ -266,6 +318,8 @@ export default { loading: ref(false), tabs: ref(1), plugins: [], + templatesLoading: false, + templates: [], paperOptions: [ { title: 'A4 افقی', value: 'A4-L' }, { title: 'A4 عمودی', value: 'A4' }, @@ -286,7 +340,8 @@ export default { discountInfo: true, paper: 'A4-L', businessStamp: true, - invoiceIndex: true + invoiceIndex: true, + templateId: null }, buy: { pays: true, @@ -296,6 +351,7 @@ export default { taxInfo: true, discountInfo: true, paper: 'A4-L', + templateId: null }, rfbuy: { pays: true, @@ -305,6 +361,7 @@ export default { taxInfo: true, discountInfo: true, paper: 'A4-L', + templateId: null }, rfsell: { pays: true, @@ -314,6 +371,7 @@ export default { taxInfo: true, discountInfo: true, paper: 'A4-L', + templateId: null }, repservice: { noteString: '', @@ -330,9 +388,22 @@ export default { isPluginActive(plugName) { return this.plugins[plugName] !== undefined; }, + async loadTemplates() { + if (!this.isPluginActive('custominvoice')) return; + try { + this.templatesLoading = true; + const res = await axios.post('/api/plugins/custominvoice/template/list', { page: 1, itemsPerPage: 100, scope: 'business' }); + this.templates = res.data.items || []; + } finally { + this.templatesLoading = false; + } + }, getStaticData(type, key) { return this.$store.getters.getStaticData(type, key); }, + templateTitle(t) { + return t.name + (t.isPublic ? ' (عمومی)' : ''); + }, submit() { this.loading = true; axios.post('/api/printers/options/save', this.settings).then((response) => { @@ -347,6 +418,12 @@ export default { }) } }, + computed: { + templateOptions() { + const base = [{ title: 'قالب پیش‌فرض سیستم', value: null }]; + return base.concat(this.templates.map(t => ({ title: t.name, value: t.id }))); + } + }, async beforeMount() { this.loading = true; axios.post("/api/printers/options/info").then((response) => { @@ -359,6 +436,7 @@ export default { //get active plugins axios.post('/api/plugin/get/actives',).then((response) => { this.plugins = response.data; + this.loadTemplates(); }); } }