addcustom invoice template plugin
This commit is contained in:
parent
e9f2a14a27
commit
ba9fe02ce7
30
hesabixCore/migrations/Version20250809100001.php
Normal file
30
hesabixCore/migrations/Version20250809100001.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20250809100001 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create custom_invoice_template table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
25
hesabixCore/migrations/Version20250809103000.php
Normal file
25
hesabixCore/migrations/Version20250809103000.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20250809103000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add copy_count to custom_invoice_template';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
43
hesabixCore/migrations/Version20250809112000.php
Normal file
43
hesabixCore/migrations/Version20250809112000.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20250809112000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add template relations to print_options for sell/buy/rfbuy/rfsell';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this migration is auto-generated, adjust table names if needed
|
||||
$this->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');
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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' => '<b>یادداشت آزمایشی:</b> این فقط پیش فیمایش است.',
|
||||
];
|
||||
}
|
||||
|
||||
#[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']);
|
||||
}
|
||||
}
|
|
@ -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']);
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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']
|
||||
);
|
||||
|
|
112
hesabixCore/src/Entity/CustomInvoiceTemplate.php
Normal file
112
hesabixCore/src/Entity/CustomInvoiceTemplate.php
Normal file
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\CustomInvoiceTemplateRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: CustomInvoiceTemplateRepository::class)]
|
||||
class CustomInvoiceTemplate
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Business $bid = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $submitter = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $isPublic = false;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
private ?string $code = null;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $copyCount = 0;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\CustomInvoiceTemplate;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<CustomInvoiceTemplate>
|
||||
*/
|
||||
class CustomInvoiceTemplateRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, CustomInvoiceTemplate::class);
|
||||
}
|
||||
}
|
66
hesabixCore/src/Service/CustomInvoice/TemplateRenderer.php
Normal file
66
hesabixCore/src/Service/CustomInvoice/TemplateRenderer.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service\CustomInvoice;
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Sandbox\SecurityPolicy;
|
||||
use Symfony\Bridge\Twig\Extension\RoutingExtension;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class TemplateRenderer
|
||||
{
|
||||
private UrlGeneratorInterface $urlGenerator;
|
||||
|
||||
public function __construct(UrlGeneratorInterface $urlGenerator)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
|
@ -4,27 +4,27 @@
|
|||
<head>
|
||||
<style>
|
||||
.center {
|
||||
text-align: center;
|
||||
text-align: center ;
|
||||
}
|
||||
.text-white {
|
||||
color: white;
|
||||
color: white ;
|
||||
}
|
||||
.stimol td,
|
||||
.stimol th {
|
||||
border: 1px solid black;
|
||||
border: 1px solid black ;
|
||||
}
|
||||
.item {
|
||||
height: 30px;
|
||||
font-size: 11px;
|
||||
height: 30px ;
|
||||
font-size: 11px ;
|
||||
}
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
font-size: 14px ;
|
||||
}
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
font-size: 12px ;
|
||||
}
|
||||
p {
|
||||
font-size: 11px;
|
||||
font-size: 11px ;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
@ -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 %}
|
||||
<tr class="stimol">
|
||||
<td class="center item">{{rowIndex}}</td>
|
||||
|
@ -304,16 +304,16 @@
|
|||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{# فیلد جدید وضعیت حساب مشتری #}
|
||||
{ # فیلد جدید وضعیت حساب مشتری #}
|
||||
{% if accountStatus is defined %}
|
||||
<h4 class="">
|
||||
وضعیت حساب مشتری با احتساب این فاکتور:
|
||||
{{ accountStatus.value | number_format}}
|
||||
{{ doc.money.shortName }}
|
||||
{{ accountStatus.label }}
|
||||
|
||||
|
||||
</h4>
|
||||
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</h4>
|
||||
|
@ -371,4 +371,3 @@
|
|||
</div>
|
||||
</body>
|
||||
</body></div></body></html>
|
||||
|
||||
|
|
|
@ -5,30 +5,174 @@
|
|||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import * as monaco from 'monaco-editor'
|
||||
// Vite-friendly worker imports
|
||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
|
||||
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
|
||||
import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
|
||||
import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
|
||||
import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
|
||||
|
||||
// تنظیمات MonacoEnvironment برای web workers
|
||||
if (typeof self !== 'undefined' && !self.MonacoEnvironment) {
|
||||
// Register Twig language and completion once per app lifetime
|
||||
let twigEnhancementsRegistered = false
|
||||
|
||||
function registerTwigLanguageAndCompletions() {
|
||||
if (twigEnhancementsRegistered) return
|
||||
try {
|
||||
// Register Twig language
|
||||
monaco.languages.register({ id: 'twig', extensions: ['.twig'], aliases: ['Twig', 'twig'] })
|
||||
|
||||
// Basic Twig tokenizer
|
||||
monaco.languages.setMonarchTokensProvider('twig', {
|
||||
tokenPostfix: '.twig',
|
||||
defaultToken: '',
|
||||
brackets: [
|
||||
{ open: '{{', close: '}}', token: 'delimiter.curly' },
|
||||
{ open: '{%', close: '%}', token: 'delimiter.curly' },
|
||||
{ open: '{#', close: '#}', token: 'comment' }
|
||||
],
|
||||
keywords: [
|
||||
'if', 'elseif', 'else', 'endif',
|
||||
'for', 'endfor', 'in',
|
||||
'set', 'include', 'extends', 'with', 'only',
|
||||
'block', 'endblock', 'macro', 'import', 'from', 'filter', 'endfilter'
|
||||
],
|
||||
filters: [
|
||||
'upper', 'lower', 'capitalize', 'title', 'trim', 'replace', 'default', 'length', 'escape', 'raw', 'number_format'
|
||||
],
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/\{#/, 'comment', '@comment'],
|
||||
[/\{\{/, 'delimiter.twig', '@variable'],
|
||||
[/\{%/, 'delimiter.twig', '@block'],
|
||||
[/[^\{]+/, ''],
|
||||
[/./, '']
|
||||
],
|
||||
comment: [
|
||||
[/[^#}]+/, 'comment.content'],
|
||||
[/#\}/, 'comment', '@pop']
|
||||
],
|
||||
variable: [
|
||||
[/\}\}/, 'delimiter.twig', '@pop'],
|
||||
[/\|\s*[a-zA-Z_][\w]*/, 'keyword'],
|
||||
[/\b([a-zA-Z_][\w]*)\b/, 'variable'],
|
||||
[/[^}]+/, '']
|
||||
],
|
||||
block: [
|
||||
[/%\}/, 'delimiter.twig', '@pop'],
|
||||
[/\b(if|elseif|else|endif|for|endfor|in|set|include|extends|with|only|block|endblock|macro|import|from|filter|endfilter)\b/, 'keyword'],
|
||||
[/\b([a-zA-Z_][\w]*)\b/, 'identifier'],
|
||||
[/[^%]+/, '']
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// Twig variables for invoice templates (autocomplete)
|
||||
const twigVariables = [
|
||||
{ label: 'company_name', detail: 'نام شرکت' },
|
||||
{ label: 'invoice_number', detail: 'شماره فاکتور' },
|
||||
{ label: 'invoice_date', detail: 'تاریخ فاکتور' },
|
||||
{ label: 'customer_name', detail: 'نام مشتری' },
|
||||
{ label: 'total_amount', detail: 'مبلغ کل' },
|
||||
{ label: 'items_list', detail: 'لیست اقلام' }
|
||||
]
|
||||
|
||||
const twigKeywords = [
|
||||
{ label: 'if', detail: 'ساختار شرطی' },
|
||||
{ label: 'elseif', detail: 'شرط جایگزین' },
|
||||
{ label: 'else', detail: 'بخش جایگزین' },
|
||||
{ label: 'endif', detail: 'پایان if' },
|
||||
{ label: 'for', detail: 'حلقه' },
|
||||
{ label: 'endfor', detail: 'پایان for' },
|
||||
{ label: 'in', detail: 'اپراتور in' },
|
||||
{ label: 'set', detail: 'تعریف متغیر' },
|
||||
{ label: 'include', detail: 'درج قالب' },
|
||||
{ label: 'extends', detail: 'ارثبری قالب' },
|
||||
{ label: 'block', detail: 'بلوک قالب' },
|
||||
{ label: 'endblock', detail: 'پایان بلوک' },
|
||||
{ label: 'filter', detail: 'اعمال فیلتر' },
|
||||
{ label: 'endfilter', detail: 'پایان فیلتر' }
|
||||
]
|
||||
|
||||
const twigSnippets = [
|
||||
{
|
||||
label: 'if ... endif',
|
||||
detail: 'الگوی if',
|
||||
insertText: '{% if ${1:condition} %}\n ${2:...}\n{% endif %}',
|
||||
},
|
||||
{
|
||||
label: 'for ... endfor',
|
||||
detail: 'الگوی for',
|
||||
insertText: '{% for ${1:item} in ${2:items} %}\n ${3:...}\n{% endfor %}'
|
||||
}
|
||||
]
|
||||
|
||||
const mapToMonacoItems = (items, kind) => items.map((it) => ({
|
||||
label: it.label,
|
||||
kind,
|
||||
insertText: it.insertText || it.label,
|
||||
insertTextRules: it.insertText ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet : undefined,
|
||||
detail: it.detail
|
||||
}))
|
||||
|
||||
// Utility: determine if cursor is inside a Twig pair on the current line
|
||||
function isInsideTwigContext(model, position) {
|
||||
const lineText = model.getLineContent(position.lineNumber)
|
||||
const textUntilPos = lineText.substring(0, position.column - 1)
|
||||
const openVar = textUntilPos.lastIndexOf('{{')
|
||||
const closeVar = textUntilPos.lastIndexOf('}}')
|
||||
const openBlock = textUntilPos.lastIndexOf('{%')
|
||||
const closeBlock = textUntilPos.lastIndexOf('%}')
|
||||
const inVar = openVar > -1 && openVar > closeVar
|
||||
const inBlock = openBlock > -1 && openBlock > closeBlock
|
||||
return inVar || inBlock
|
||||
}
|
||||
|
||||
// Completion provider for Twig language
|
||||
monaco.languages.registerCompletionItemProvider('twig', {
|
||||
triggerCharacters: ['{', '%', ' ', '.', '_', '|'],
|
||||
provideCompletionItems(model, position) {
|
||||
const suggestions = [
|
||||
...mapToMonacoItems(twigVariables, monaco.languages.CompletionItemKind.Variable),
|
||||
...mapToMonacoItems(twigKeywords, monaco.languages.CompletionItemKind.Keyword),
|
||||
...mapToMonacoItems(twigSnippets, monaco.languages.CompletionItemKind.Snippet)
|
||||
]
|
||||
return { suggestions }
|
||||
}
|
||||
})
|
||||
|
||||
// Also provide Twig suggestions while editing HTML (mixed content)
|
||||
monaco.languages.registerCompletionItemProvider('html', {
|
||||
triggerCharacters: ['{', '%', ' ', '.', '_', '|'],
|
||||
provideCompletionItems(model, position) {
|
||||
if (!isInsideTwigContext(model, position)) return { suggestions: [] }
|
||||
const suggestions = [
|
||||
...mapToMonacoItems(twigVariables, monaco.languages.CompletionItemKind.Variable),
|
||||
...mapToMonacoItems(twigKeywords, monaco.languages.CompletionItemKind.Keyword),
|
||||
...mapToMonacoItems(twigSnippets, monaco.languages.CompletionItemKind.Snippet)
|
||||
]
|
||||
return { suggestions }
|
||||
}
|
||||
})
|
||||
|
||||
twigEnhancementsRegistered = true
|
||||
} catch (err) {
|
||||
console.warn('Failed to register Twig language/completions:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// تنظیمات MonacoEnvironment برای web workers (سازگار با Vite)
|
||||
if (typeof self !== 'undefined') {
|
||||
self.MonacoEnvironment = {
|
||||
getWorkerUrl: function (moduleId, label) {
|
||||
getWorker: function (moduleId, label) {
|
||||
try {
|
||||
// استفاده از web workers موجود در پوشه public
|
||||
if (label === 'json') {
|
||||
return '/monaco-editor/min/vs/language/json/json.worker.js'
|
||||
}
|
||||
if (label === 'css' || label === 'scss' || label === 'less') {
|
||||
return '/monaco-editor/min/vs/language/css/css.worker.js'
|
||||
}
|
||||
if (label === 'html' || label === 'handlebars' || label === 'razor') {
|
||||
return '/monaco-editor/min/vs/language/html/html.worker.js'
|
||||
}
|
||||
if (label === 'typescript' || label === 'javascript') {
|
||||
return '/monaco-editor/min/vs/language/typescript/ts.worker.js'
|
||||
}
|
||||
return '/monaco-editor/min/vs/editor/editor.worker.js'
|
||||
if (label === 'json') return new JsonWorker()
|
||||
if (label === 'css' || label === 'scss' || label === 'less') return new CssWorker()
|
||||
if (label === 'html' || label === 'handlebars' || label === 'razor') return new HtmlWorker()
|
||||
if (label === 'typescript' || label === 'javascript') return new TsWorker()
|
||||
return new EditorWorker()
|
||||
} catch (error) {
|
||||
console.warn('Monaco Editor worker not found, falling back to main thread:', error)
|
||||
// Fallback to main thread if workers fail
|
||||
return null
|
||||
console.warn('Monaco Editor worker failed to start, falling back to main thread:', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +215,9 @@ export default {
|
|||
if (!editorContainer.value) return
|
||||
|
||||
try {
|
||||
// Ensure Twig language and completions are available
|
||||
registerTwigLanguageAndCompletions()
|
||||
|
||||
// تنظیمات پیشفرض
|
||||
const defaultOptions = {
|
||||
value: props.modelValue,
|
||||
|
@ -173,6 +320,8 @@ export default {
|
|||
})
|
||||
|
||||
watch(() => props.language, (newLanguage) => {
|
||||
// Ensure Twig language is registered if needed when language changes
|
||||
registerTwigLanguageAndCompletions()
|
||||
if (editor) {
|
||||
monaco.editor.setModelLanguage(editor.getModel(), newLanguage)
|
||||
}
|
||||
|
|
|
@ -14,76 +14,23 @@
|
|||
<v-btn v-bind="props" icon="mdi-content-save" color="primary" @click="saveTemplate" :loading="saving"></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-menu>
|
||||
<v-tooltip text="پیشنمایش (HTML)" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon color="green">
|
||||
<v-icon>mdi-eye</v-icon>
|
||||
<v-tooltip activator="parent" text="پیشنمایش قالب" location="bottom" />
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-subheader color="primary">پیشنمایش قالب</v-list-subheader>
|
||||
<v-list-item class="text-dark" title="پیشنمایش در مرورگر" @click="previewTemplate">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="blue-darken-4" icon="mdi-eye"></v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item class="text-dark" title="پیشنمایش چاپ" @click="previewPrint">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="orange-darken-4" icon="mdi-printer"></v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon color="purple">
|
||||
<v-icon>mdi-palette</v-icon>
|
||||
<v-tooltip activator="parent" text="قالبهای آماده" location="bottom" />
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-subheader color="primary">قالبهای آماده</v-list-subheader>
|
||||
<v-list-item class="text-dark" title="قالب استاندارد" @click="loadTemplate('standard')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="green-darken-4" icon="mdi-file-document"></v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item class="text-dark" title="قالب لوکس" @click="loadTemplate('luxury')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="amber-darken-4" icon="mdi-star"></v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item class="text-dark" title="قالب تجاری" @click="loadTemplate('business')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="blue-darken-4" icon="mdi-briefcase"></v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-tooltip text="فرمت کردن کد" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon="mdi-format-align-left" color="info" @click="formatCode" />
|
||||
<v-btn v-bind="props" icon="mdi-eye" class="ml-2" @click="previewHtml" :disabled="!templateData.code"></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip text="تنظیمات قالب" location="bottom">
|
||||
<v-tooltip text="دانلود پیشنمایش PDF" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon="mdi-cog" color="primary" @click="showSettings = true" />
|
||||
<v-btn v-bind="props" icon="mdi-file-pdf-box" color="secondary" class="ml-2" @click="previewPdf" :disabled="!templateData.code"></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-toolbar>
|
||||
|
||||
<v-tabs v-model="activeTab" color="primary" class="template-tabs">
|
||||
<v-tab value="form">فرم قالب</v-tab>
|
||||
<v-tab value="help">راهنما و آموزش</v-tab>
|
||||
<v-tab value="settings">تنظیمات ویرایشگر</v-tab>
|
||||
</v-tabs>
|
||||
<v-container>
|
||||
<v-tabs v-model="activeTab" color="primary" class="template-tabs">
|
||||
<v-tab value="form">فرم قالب</v-tab>
|
||||
<v-tab value="help">راهنما و آموزش</v-tab>
|
||||
<v-tab value="settings">تنظیمات ویرایشگر</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="activeTab" class="template-window">
|
||||
<!-- فرم قالب -->
|
||||
<v-window-item value="form">
|
||||
|
@ -102,156 +49,324 @@
|
|||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-label class="text-subtitle-2 mb-2 d-block">کد قالب</v-label>
|
||||
<MonacoEditor
|
||||
v-model="templateData.code"
|
||||
:language="editorSettings.language"
|
||||
:theme="editorSettings.theme"
|
||||
height="500px"
|
||||
:options="monacoOptions"
|
||||
@change="onCodeChange"
|
||||
ref="monacoEditor"
|
||||
/>
|
||||
<MonacoEditor v-model="templateData.code" :language="editorSettings.language"
|
||||
:theme="editorSettings.theme" height="500px" :options="monacoOptions" @change="onCodeChange"
|
||||
ref="monacoEditor" />
|
||||
<div v-if="codeError" class="text-error text-caption mt-1">
|
||||
{{ codeError }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<div class="form-actions">
|
||||
<v-btn color="primary" size="large" @click="saveTemplate" :loading="saving">
|
||||
<v-icon left>mdi-content-save</v-icon>
|
||||
{{ isEditMode ? 'بروزرسانی قالب' : 'ذخیره قالب' }}
|
||||
</v-btn>
|
||||
<v-btn color="secondary" size="large" @click="goBack" class="ml-3">
|
||||
<v-icon left>mdi-arrow-right</v-icon>
|
||||
بازگشت
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-window-item>
|
||||
|
||||
<!-- راهنما و آموزش -->
|
||||
<v-window-item value="help">
|
||||
<v-container class="help-content">
|
||||
<!-- مقدمه -->
|
||||
<v-card class="help-section mb-6" variant="outlined">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon color="primary" class="mr-3">mdi-information</v-icon>
|
||||
راهنمای ایجاد قالب
|
||||
راهنمای جامع طراحی قالب فاکتور سفارشی
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list class="help-steps">
|
||||
<v-list-item class="help-step mb-4">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar color="primary" size="30" class="step-number">
|
||||
<span class="text-white font-weight-bold">1</span>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="text-h6 mb-2">نام قالب</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
نام مناسبی برای قالب خود انتخاب کنید که نشاندهنده نوع و کاربرد آن باشد.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="help-step mb-4">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar color="primary" size="30" class="step-number">
|
||||
<span class="text-white font-weight-bold">2</span>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="text-h6 mb-2">وضعیت عمومی</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
اگر قالب را عمومی انتخاب کنید، سایر کاربران نیز میتوانند از آن استفاده کنند.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="help-step mb-4">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar color="primary" size="30" class="step-number">
|
||||
<span class="text-white font-weight-bold">3</span>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="text-h6 mb-2">کد قالب</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
کد HTML و CSS قالب خود را در این قسمت وارد کنید. میتوانید از متغیرهای زیر استفاده کنید:
|
||||
</v-list-item-subtitle>
|
||||
<v-card class="code-variables mt-3" variant="outlined">
|
||||
<v-card-text class="font-family-monospace">
|
||||
<v-chip color="error" size="small" class="mr-2 mb-1" v-text="'{{ company_name }}'"></v-chip> - نام شرکت<br>
|
||||
<v-chip color="error" size="small" class="mr-2 mb-1" v-text="'{{ invoice_number }}'"></v-chip> - شماره
|
||||
فاکتور<br>
|
||||
<v-chip color="error" size="small" class="mr-2 mb-1" v-text="'{{ invoice_date }}'"></v-chip> - تاریخ
|
||||
فاکتور<br>
|
||||
<v-chip color="error" size="small" class="mr-2 mb-1" v-text="'{{ customer_name }}'"></v-chip> - نام
|
||||
مشتری<br>
|
||||
<v-chip color="error" size="small" class="mr-2 mb-1" v-text="'{{ total_amount }}'"></v-chip> - مبلغ کل<br>
|
||||
<v-chip color="error" size="small" class="mr-2 mb-1" v-text="'{{ items_list }}'"></v-chip> - لیست اقلام
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<p>
|
||||
در این بخش میتوانید با استفاده از زبان قالببندی Twig و HTML/CSS، قالب فاکتور سفارشی خود را طراحی کنید.
|
||||
برای جلوگیری از مشکلات امنیتی، رندر کدها در <b>Sandbox</b> انجام میشود؛ بنابراین تنها بخشی از امکانات Twig مجاز است.
|
||||
</p>
|
||||
<ul class="mt-3">
|
||||
<li>تگهای مجاز: <b>if</b> و <b>for</b></li>
|
||||
<li>فیلترهای مجاز: <b>escape</b>، <b>upper</b>، <b>lower</b>، <b>number_format</b>، <b>join</b></li>
|
||||
<li>تابع/متد مستقیم غیرفعال است؛ فقط از دادههای فراهمشده در کانتکست استفاده کنید.</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- متغیرهای در دسترس -->
|
||||
<v-card class="help-section mb-6" variant="outlined">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon color="primary" class="mr-3">mdi-database</v-icon>
|
||||
متغیرهای در دسترس در قالب
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p>در زمان رندر، دادههای زیر در اختیار قالب قرار میگیرد:</p>
|
||||
<ul class="mt-3">
|
||||
<li>
|
||||
<b>business</b>: اطلاعات کسبوکار
|
||||
<ul>
|
||||
<li><code>business.name</code>, <code>business.tel</code>, <code>business.mobile</code>, <code>business.address</code></li>
|
||||
<li><code>business.shenasemeli</code>, <code>business.codeeghtesadi</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>doc</b>: اطلاعات سند
|
||||
<ul>
|
||||
<li><code>doc.code</code>, <code>doc.date</code></li>
|
||||
<li>در فروش: <code>doc.taxPercent</code>, <code>doc.discountPercent</code>, <code>doc.discountType</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>person</b>: اطلاعات مشتری/طرف حساب
|
||||
<ul>
|
||||
<li><code>person.name</code>, <code>person.mobile</code>, <code>person.tel</code>, <code>person.address</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>rows</b>: آرایه اقلام فاکتور (هر ردیف یک شیء)
|
||||
<ul>
|
||||
<li><code>row.commodity.name</code>, <code>row.commodity.code</code> (ممکن است <code>commodity</code> تهی باشد)</li>
|
||||
<li><code>row.commodityCount</code> (و برای سازگاری قدیمی: <code>row.commdityCount</code>)</li>
|
||||
<li><code>row.des</code>, <code>row.bs</code>, <code>row.bd</code> (در خرید/برگشت از خرید)، <code>row.tax</code>, <code>row.discount</code></li>
|
||||
<li>در فروش/برگشت از فروش: <code>row.showPercentDiscount</code>, <code>row.discountPercent</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>discount</b> و <b>transfer</b>: جمع تخفیف و هزینه ارسال/انتقال در سطح فاکتور (در صورت وجود)
|
||||
</li>
|
||||
<li>
|
||||
<b>note</b>: یادداشت پایین فاکتور (از تنظیمات چاپ)
|
||||
</li>
|
||||
<li>
|
||||
<b>printOptions</b>: تنظیمات چاپ جاری (مانند <code>paper</code>، <code>bidInfo</code>، ...)
|
||||
</li>
|
||||
<li>
|
||||
در فروش: <b>accountStatus</b> با کلیدهای <code>label</code> و <code>value</code>
|
||||
</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- نمونه: حلقه و شرط -->
|
||||
<v-card class="help-section mb-6" variant="outlined">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon color="primary" class="mr-3">mdi-code-tags</v-icon>
|
||||
نمونه کد قالب
|
||||
مثالهای کاربردی Twig (حلقهها و شرطها)
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-card class="code-example" variant="outlined">
|
||||
<div class="code-example" variant="outlined">
|
||||
<v-card-text class="font-family-monospace">
|
||||
<pre class="text-body-2"><code v-text="codeExample"></code></pre>
|
||||
<pre class="text-body-2" v-pre><code>{% if person %}
|
||||
<p>مشتری: {{ person.name | escape }}</p>
|
||||
{% endif %}
|
||||
|
||||
<table width="100%" cellspacing="0" cellpadding="6" border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>کالا</th>
|
||||
<th>تعداد</th>
|
||||
<th>فی توضیح</th>
|
||||
<th>مالیات</th>
|
||||
<th>تخفیف</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in rows %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ item.commodity.name ?? '-' }}</td>
|
||||
<td>{{ item.commodityCount }}</td>
|
||||
<td>{{ item.des | escape }}</td>
|
||||
<td>{{ item.tax | number_format(0, '.', ',') }}</td>
|
||||
<td>
|
||||
{% if item.showPercentDiscount %}
|
||||
{{ item.discountPercent }}%
|
||||
{% else %}
|
||||
{{ item.discount | number_format(0, '.', ',') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</code></pre>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="help-section" variant="outlined">
|
||||
<!-- نمونه کامل فروش -->
|
||||
<v-card class="help-section mb-6" variant="outlined">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon color="primary" class="mr-3">mdi-lightbulb</v-icon>
|
||||
نکات مهم
|
||||
<v-icon color="primary" class="mr-3">mdi-file-document</v-icon>
|
||||
نمونه کامل برای فاکتور فروش
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list class="tips-list">
|
||||
<v-list-item class="tips-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success" class="mr-3">mdi-check</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>از CSS برای زیبایی قالب استفاده کنید</v-list-item-title>
|
||||
</v-list-item>
|
||||
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
||||
<h2 style="text-align:center">{{ business.name }}</h2>
|
||||
<div>شماره فاکتور: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||
{% if person %}<div>مشتری: {{ person.name }} | موبایل: {{ person.mobile }}</div>{% endif %}
|
||||
|
||||
<v-list-item class="tips-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success" class="mr-3">mdi-check</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>مطمئن شوید که قالب در چاپ به خوبی نمایش داده میشود</v-list-item-title>
|
||||
</v-list-item>
|
||||
<table width="100%" cellspacing="0" cellpadding="6" border="1" style="margin-top:10px">
|
||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>مالیات</th><th>تخفیف</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ r.commodity.name ?? '-' }}</td>
|
||||
<td>{{ r.commodityCount }}</td>
|
||||
<td>{{ r.des }}</td>
|
||||
<td>{{ r.tax | number_format(0, '.', ',') }}</td>
|
||||
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount | number_format(0, '.', ',') }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<v-list-item class="tips-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success" class="mr-3">mdi-check</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>از فونتهای فارسی استفاده کنید</v-list-item-title>
|
||||
</v-list-item>
|
||||
<div style="margin-top:10px;text-align:right">
|
||||
{% if discount %}<div>جمع تخفیف: {{ discount | number_format(0, '.', ',') }}</div>{% endif %}
|
||||
{% if transfer %}<div>هزینه ارسال/انتقال: {{ transfer | number_format(0, '.', ',') }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<v-list-item class="tips-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success" class="mr-3">mdi-check</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>رنگبندی مناسب برای خوانایی انتخاب کنید</v-list-item-title>
|
||||
</v-list-item>
|
||||
{% if note %}
|
||||
<div style="margin-top:12px;border-top:1px dashed #ccc;padding-top:8px">{{ note | escape }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</code></pre>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- نمونه کامل خرید / برگشت از خرید -->
|
||||
<v-card class="help-section mb-6" variant="outlined">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon color="primary" class="mr-3">mdi-file-document-outline</v-icon>
|
||||
نمونه کامل برای خرید / برگشت از خرید
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p>در خرید/برگشت از خرید، مقادیر ردیفها شامل <code>bs</code> و <code>bd</code> نیز هستند.</p>
|
||||
<pre class="text-body-2" v-pre><code>{% for r in rows %}
|
||||
<div>{{ loop.index }}. {{ r.commodity.name ?? '-' }} | تعداد: {{ r.commodityCount }} | شرح: {{ r.des }} | بدهکار: {{ r.bd }} | بستانکار: {{ r.bs }}</div>
|
||||
{% endfor %}
|
||||
</code></pre>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- نکات امنیتی و محدودیتها -->
|
||||
<v-card class="help-section mb-6" variant="outlined">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon color="primary" class="mr-3">mdi-shield-lock</v-icon>
|
||||
نکات امنیتی و محدودیتها
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<ul>
|
||||
<li>از اجرای جاوااسکریپت، توابع سیستم یا درخواستهای خارجی در قالب پرهیز شده و امکانپذیر نیست.</li>
|
||||
<li>برای جلوگیری از خطاهای سازگاری، از <b>commodityCount</b> استفاده کنید (هرچند برای سازگاری <b>commdityCount</b> نیز پشتیبانی میشود).</li>
|
||||
<li>به دلیل Sandbox، دسترسی به متد/پراپرتیهای آبجکتها محدود است؛ از دادههای آرایهای فراهمشده استفاده کنید.</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- عیبیابی متداول -->
|
||||
<v-card class="help-section" variant="outlined">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon color="primary" class="mr-3">mdi-lifebuoy</v-icon>
|
||||
عیبیابی متداول
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<ul>
|
||||
<li>خطای «Key X does not exist»: نام کلید را با فهرست بالا تطبیق دهید؛ برای تعداد از <code>commodityCount</code> استفاده کنید.</li>
|
||||
<li>عدم نمایش اطلاعات مشتری: ابتدا بررسی کنید <code>person</code> تهی نباشد: <code>{% if person %} ... {% endif %}</code></li>
|
||||
<li>بههمریختگی چاپ: از CSS ساده و سازگار با چاپ استفاده کنید؛ عرض جدولها و اندازه فونتها را کنترل کنید.</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- قالبهای پیشفرض: فروش، خرید، برگشتها -->
|
||||
<v-card class="help-section mb-6" variant="outlined">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon color="primary" class="mr-3">mdi-format-list-text</v-icon>
|
||||
قالبهای پیشفرض (نمونههای آماده)
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p>در ادامه نمونههایی از قالبهای پیشفرض سیستم برای الهام گرفتن آورده شده است.</p>
|
||||
|
||||
<h4 class="mt-4">فروش (نمونه ساده)</h4>
|
||||
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
||||
<h3 style="text-align:center">{{ business.name }}</h3>
|
||||
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||
{% if person %}<div>مشتری: {{ person.name }}</div>{% endif %}
|
||||
<table width="100%" border="1" cellspacing="0" cellpadding="6" style="margin-top:8px">
|
||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>مالیات</th><th>تخفیف</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ r.commodity.name ?? '-' }}</td>
|
||||
<td>{{ r.commodityCount }}</td>
|
||||
<td>{{ r.des }}</td>
|
||||
<td>{{ r.tax | number_format(0, '.', ',') }}</td>
|
||||
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount | number_format(0, '.', ',') }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if note %}<div style="margin-top:8px">{{ note | escape }}</div>{% endif %}
|
||||
</div></code></pre>
|
||||
|
||||
<h4 class="mt-6">خرید (نمونه ساده)</h4>
|
||||
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
||||
<h3 style="text-align:center">{{ business.name }}</h3>
|
||||
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||
{% if person %}<div>فروشنده: {{ person.name }}</div>{% endif %}
|
||||
<table width="100%" border="1" cellspacing="0" cellpadding="6" style="margin-top:8px">
|
||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>بدهکار</th><th>بستانکار</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ r.commodity.name ?? '-' }}</td>
|
||||
<td>{{ r.commodityCount }}</td>
|
||||
<td>{{ r.des }}</td>
|
||||
<td>{{ r.bd | number_format(0, '.', ',') }}</td>
|
||||
<td>{{ r.bs | number_format(0, '.', ',') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div></code></pre>
|
||||
|
||||
<h4 class="mt-6">برگشت از خرید (نمونه ساده)</h4>
|
||||
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
||||
<h3 style="text-align:center">{{ business.name }}</h3>
|
||||
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||
<table width="100%" border="1" cellspacing="0" cellpadding="6" style="margin-top:8px">
|
||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>بدهکار</th><th>بستانکار</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ r.commodity.name ?? '-' }}</td>
|
||||
<td>{{ r.commodityCount }}</td>
|
||||
<td>{{ r.des }}</td>
|
||||
<td>{{ r.bd | number_format(0, '.', ',') }}</td>
|
||||
<td>{{ r.bs | number_format(0, '.', ',') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div></code></pre>
|
||||
|
||||
<h4 class="mt-6">برگشت از فروش (نمونه ساده)</h4>
|
||||
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
||||
<h3 style="text-align:center">{{ business.name }}</h3>
|
||||
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||
<table width="100%" border="1" cellspacing="0" cellpadding="6" style="margin-top:8px">
|
||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>مالیات</th><th>تخفیف</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ r.commodity.name ?? '-' }}</td>
|
||||
<td>{{ r.commodityCount }}</td>
|
||||
<td>{{ r.des }}</td>
|
||||
<td>{{ r.tax | number_format(0, '.', ',') }}</td>
|
||||
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount | number_format(0, '.', ',') }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div></code></pre>
|
||||
|
||||
<v-list-item class="tips-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success" class="mr-3">mdi-check</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>قبل از ذخیره، قالب را تست کنید</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
@ -272,23 +387,25 @@
|
|||
color="primary" @update:model-value="applySettings" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-switch v-model="editorSettings.wordWrap" label="شکستن خودکار خط" color="primary" @update:model-value="applySettings" />
|
||||
<v-switch v-model="editorSettings.wordWrap" label="شکستن خودکار خط" color="primary"
|
||||
@update:model-value="applySettings" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-switch v-model="editorSettings.minimap" label="نمایش مینیمپ" color="primary"
|
||||
@update:model-value="applySettings" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-switch v-model="editorSettings.autoComplete" label="تکمیل خودکار کد" color="primary"
|
||||
@update:model-value="applySettings" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-switch v-model="editorSettings.minimap" label="نمایش مینیمپ" color="primary" @update:model-value="applySettings" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-switch v-model="editorSettings.autoComplete" label="تکمیل خودکار کد" color="primary" @update:model-value="applySettings" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select v-model="editorSettings.theme" label="تم ویرایشگر" :items="themeOptions"
|
||||
variant="outlined" @update:model-value="applySettings" />
|
||||
<v-select v-model="editorSettings.theme" label="تم ویرایشگر" :items="themeOptions" variant="outlined"
|
||||
@update:model-value="applySettings" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select v-model="editorSettings.language" label="زبان کد" :items="languageOptions"
|
||||
|
@ -335,6 +452,7 @@
|
|||
|
||||
<script>
|
||||
import MonacoEditor from '@/components/MonacoEditor.vue'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'CustomInvoiceTemplateForm',
|
||||
|
@ -386,8 +504,9 @@ export default {
|
|||
{ title: 'HTML', value: 'html' },
|
||||
{ title: 'CSS', value: 'css' },
|
||||
{ title: 'JavaScript', value: 'javascript' },
|
||||
{ title: 'TypeScript', value: 'typescript' }
|
||||
]
|
||||
{ title: 'TypeScript', value: 'typescript' },
|
||||
{ title: 'Twig', value: 'twig' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -427,21 +546,54 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
saveTemplate() {
|
||||
// Validation
|
||||
async loadExistingTemplate() {
|
||||
try {
|
||||
const { data } = await axios.get(`/api/plugins/custominvoice/template/${this.templateId}`)
|
||||
const t = data?.data
|
||||
if (t) {
|
||||
this.templateData.name = t.name || ''
|
||||
this.templateData.isPublic = !!t.isPublic
|
||||
this.templateData.code = t.code || ''
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.message || 'خطا در بارگذاری قالب'
|
||||
this.$toast?.error(msg)
|
||||
}
|
||||
},
|
||||
async saveTemplate() {
|
||||
if (!this.templateData.name || !this.templateData.code) {
|
||||
// Show error message
|
||||
this.$toast?.error('نام و کد قالب الزامی است.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const payload = {
|
||||
name: this.templateData.name,
|
||||
isPublic: this.templateData.isPublic,
|
||||
code: this.templateData.code,
|
||||
};
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
this.saving = false;
|
||||
// Navigate back to templates list
|
||||
let response;
|
||||
if (this.isEditMode && this.templateId) {
|
||||
response = await axios.post(`/api/plugins/custominvoice/template/${this.templateId}`, payload);
|
||||
} else {
|
||||
response = await axios.post('/api/plugins/custominvoice/template', payload);
|
||||
}
|
||||
|
||||
const data = response.data?.data || {};
|
||||
if (!this.isEditMode) {
|
||||
this.templateId = data.id;
|
||||
this.isEditMode = true;
|
||||
}
|
||||
this.$toast?.success('قالب با موفقیت ذخیره شد.');
|
||||
this.$router.push('/acc/plugins/custominvoice/templates');
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.message || 'خطا در ذخیره قالب';
|
||||
this.$toast?.error(msg);
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
goBack() {
|
||||
this.$router.push('/acc/plugins/custominvoice/templates');
|
||||
|
@ -495,7 +647,6 @@ export default {
|
|||
onCodeChange(value) {
|
||||
this.codeError = '';
|
||||
try {
|
||||
// Attempt to parse the HTML to check for errors
|
||||
new DOMParser().parseFromString(value, 'text/html');
|
||||
} catch (e) {
|
||||
this.codeError = 'کد HTML معتبر نیست. لطفاً خطاهای آن را برطرف کنید.';
|
||||
|
@ -537,13 +688,13 @@ export default {
|
|||
applySettings() {
|
||||
// جلوگیری از اعمال مکرر تنظیمات
|
||||
if (this.applyingSettings) return;
|
||||
|
||||
|
||||
this.applyingSettings = true;
|
||||
|
||||
|
||||
// اعمال تنظیمات روی Monaco Editor
|
||||
if (this.$refs.monacoEditor && this.$refs.monacoEditor.editor) {
|
||||
const editor = this.$refs.monacoEditor.editor;
|
||||
|
||||
|
||||
// اعمال تنظیمات جدید
|
||||
editor.updateOptions({
|
||||
fontSize: this.editorSettings.fontSize,
|
||||
|
@ -557,17 +708,17 @@ export default {
|
|||
acceptSuggestionOnEnter: this.editorSettings.autoComplete ? 'on' : 'off',
|
||||
tabCompletion: this.editorSettings.autoComplete ? 'on' : 'off'
|
||||
});
|
||||
|
||||
|
||||
// تغییر تم
|
||||
if (window.monaco) {
|
||||
window.monaco.editor.setTheme(this.editorSettings.theme);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ذخیره تنظیمات در localStorage
|
||||
localStorage.setItem('monacoEditorSettings', JSON.stringify(this.editorSettings));
|
||||
localStorage.setItem('appearanceSettings', JSON.stringify(this.appearanceSettings));
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
this.applyingSettings = false;
|
||||
// نمایش پیام موفقیت فقط در صورت نیاز
|
||||
|
@ -590,11 +741,11 @@ export default {
|
|||
primaryColor: '#667eea',
|
||||
secondaryColor: '#42b883'
|
||||
};
|
||||
|
||||
|
||||
// اعمال تنظیمات بازنشانی شده روی Monaco Editor
|
||||
if (this.$refs.monacoEditor && this.$refs.monacoEditor.editor) {
|
||||
const editor = this.$refs.monacoEditor.editor;
|
||||
|
||||
|
||||
editor.updateOptions({
|
||||
fontSize: this.editorSettings.fontSize,
|
||||
wordWrap: this.editorSettings.wordWrap ? 'on' : 'off',
|
||||
|
@ -607,17 +758,17 @@ export default {
|
|||
acceptSuggestionOnEnter: this.editorSettings.autoComplete ? 'on' : 'off',
|
||||
tabCompletion: this.editorSettings.autoComplete ? 'on' : 'off'
|
||||
});
|
||||
|
||||
|
||||
// تغییر تم
|
||||
if (window.monaco) {
|
||||
window.monaco.editor.setTheme(this.editorSettings.theme);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// پاک کردن تنظیمات از localStorage
|
||||
localStorage.removeItem('monacoEditorSettings');
|
||||
localStorage.removeItem('appearanceSettings');
|
||||
|
||||
|
||||
this.$toast?.success('تنظیمات با موفقیت بازنشانی شد.');
|
||||
},
|
||||
loadSavedSettings() {
|
||||
|
@ -627,7 +778,7 @@ export default {
|
|||
if (savedEditorSettings) {
|
||||
this.editorSettings = { ...this.editorSettings, ...JSON.parse(savedEditorSettings) };
|
||||
}
|
||||
|
||||
|
||||
// بارگذاری تنظیمات ظاهری
|
||||
const savedAppearanceSettings = localStorage.getItem('appearanceSettings');
|
||||
if (savedAppearanceSettings) {
|
||||
|
@ -636,14 +787,73 @@ export default {
|
|||
} catch (error) {
|
||||
console.error('خطا در بارگذاری تنظیمات:', error);
|
||||
}
|
||||
},
|
||||
async previewHtml() {
|
||||
try {
|
||||
const { data } = await axios.post('/api/plugins/custominvoice/template/preview', {
|
||||
code: this.templateData.code,
|
||||
paper: 'A4-L'
|
||||
})
|
||||
if (data?.status === 'ok') {
|
||||
const w = window.open('', '_blank')
|
||||
w.document.write(data.html)
|
||||
w.document.close()
|
||||
if (data.warning && this.$toast) this.$toast.info(data.warning)
|
||||
} else {
|
||||
const msg = data?.message || 'خطا در پیشنمایش'
|
||||
this.$toast?.error(msg)
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.message || 'خطا در پیشنمایش'
|
||||
this.$toast?.error(msg)
|
||||
}
|
||||
},
|
||||
async previewPdf() {
|
||||
try {
|
||||
// Request preview generation to get printId
|
||||
const { data } = await axios.post('/api/plugins/custominvoice/template/preview', {
|
||||
code: this.templateData.code,
|
||||
paper: 'A4-L'
|
||||
})
|
||||
if (data?.status !== 'ok' || !data.printId) {
|
||||
const msg = data?.message || 'خطا در ایجاد پیشنمایش PDF'
|
||||
this.$toast?.error(msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch the PDF binary like sell list implementation
|
||||
const pdfResponse = await axios({
|
||||
method: 'get',
|
||||
url: `/front/print/${data.printId}`,
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
|
||||
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
const baseName = this.templateData.name ? this.templateData.name : 'پیشنمایش قالب'
|
||||
link.setAttribute('download', `${baseName}.pdf`)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
if (data.warning && this.$toast) this.$toast.info(data.warning)
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.message || 'خطا در دریافت فایل PDF'
|
||||
this.$toast?.error(msg)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Check if we're in edit mode by checking the route parameter
|
||||
this.templateId = this.$route.params.id;
|
||||
this.isEditMode = !!this.templateId;
|
||||
|
||||
// بارگذاری تنظیمات ذخیره شده از localStorage
|
||||
if (this.isEditMode) {
|
||||
this.loadExistingTemplate();
|
||||
}
|
||||
|
||||
this.loadSavedSettings();
|
||||
|
||||
if (this.$store) {
|
||||
|
|
|
@ -1,224 +1,334 @@
|
|||
<template>
|
||||
<div class="templates-container">
|
||||
<div class="templates-header">
|
||||
<h1>مدیریت قالبهای فاکتور</h1>
|
||||
<p class="templates-description">
|
||||
طراحی و مدیریت قالبهای اختصاصی فاکتورهای فروش
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<v-toolbar color="toolbar" title="قالبهای فاکتور سفارشی">
|
||||
<template v-slot:prepend>
|
||||
<v-tooltip text="بازگشت" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" icon="mdi-arrow-right" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<div class="templates-content">
|
||||
<div class="templates-placeholder">
|
||||
<div class="placeholder-icon">
|
||||
<i class="fas fa-palette"></i>
|
||||
</div>
|
||||
<h3>بخش قالبهای فاکتور</h3>
|
||||
<p>این بخش در حال توسعه است و به زودی در دسترس خواهد بود.</p>
|
||||
<div class="placeholder-features">
|
||||
<router-link to="/acc/plugins/custominvoice/template/mod/" class="feature-item-link">
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span>ایجاد قالب جدید</span>
|
||||
</div>
|
||||
</router-link>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-edit"></i>
|
||||
<span>ویرایش قالبها</span>
|
||||
<v-tooltip text="قالب جدید" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/plugins/custominvoice/template/mod/" :disabled="activeTab !== 'business'"></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-toolbar>
|
||||
|
||||
<v-container class="pa-0 ma-0">
|
||||
<v-card :loading="loading" :disabled="loading">
|
||||
<v-card-text class="pa-0">
|
||||
<v-tabs v-model="activeTab" color="primary" class="px-4">
|
||||
<v-tab value="business">قالبهای کسبوکار</v-tab>
|
||||
<v-tab value="public">قالبهای عمومی</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<div class="pa-4">
|
||||
|
||||
<v-data-table-server
|
||||
v-model:items-per-page="serverOptions.rowsPerPage"
|
||||
v-model:page="serverOptions.page"
|
||||
v-model:sort-by="serverOptions.sortBy"
|
||||
:headers="computedHeaders"
|
||||
:items="items"
|
||||
:items-length="totalItems"
|
||||
:loading="loading"
|
||||
class="elevation-1 rounded-lg custom-table"
|
||||
:items-per-page-options="[5, 10, 20, 50]"
|
||||
item-value="id"
|
||||
items-per-page-text="تعداد سطر"
|
||||
no-data-text="اطلاعاتی برای نمایش وجود ندارد"
|
||||
:header-props="{ class: 'custom-header' }"
|
||||
density="comfortable"
|
||||
>
|
||||
<template #top>
|
||||
<v-toolbar density="comfortable" color="transparent" class="px-0">
|
||||
<v-row class="w-100 ma-0" align="center" no-gutters>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="searchValue"
|
||||
color="info"
|
||||
hide-details="auto"
|
||||
rounded="0"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="جستجو در نام یا کد قالب"
|
||||
type="text"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" class="d-flex align-center justify-end">
|
||||
<v-chip size="small" color="info" variant="flat">تعداد نتایج: {{ totalItems }}</v-chip>
|
||||
<v-btn variant="text" color="primary" icon="mdi-refresh" @click="refresh" :disabled="loading" />
|
||||
<v-btn variant="text" color="secondary" icon="mdi-close-circle" @click="clearSearch" :disabled="!searchValue" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
<template #item.operation="{ item }">
|
||||
<v-menu v-if="item && showRowActions(item)">
|
||||
<template #activator="{ props }">
|
||||
<v-btn variant="text" size="small" color="error" icon="mdi-menu" v-bind="props" />
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item v-if="activeTab === 'business'" :title="'ویرایش'" :to="'/acc/plugins/custominvoice/template/mod/' + item.id">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-file-edit"></v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="activeTab === 'business'" :title="'حذف'" @click="deleteItem(item.id)">
|
||||
<template #prepend>
|
||||
<v-icon color="deep-orange-accent-4" icon="mdi-trash-can"></v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="activeTab === 'public'" :title="'کپی و ویرایش'" @click="copyAndEdit(item.id)">
|
||||
<template #prepend>
|
||||
<v-icon color="primary" icon="mdi-content-copy"></v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<template #item.isPublic="{ item }">
|
||||
<v-icon v-if="item && item.isPublic" color="success">mdi-check</v-icon>
|
||||
<v-icon v-else-if="item" color="error">mdi-close</v-icon>
|
||||
</template>
|
||||
|
||||
<template #item.submitter="{ item }">
|
||||
<span v-if="item && item.submitter">
|
||||
{{ item.submitter.fullName || item.submitter.email }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #item.name="{ item }">
|
||||
<div class="name-cell">
|
||||
<v-icon v-if="item && item.isPublic" size="18" color="success">mdi-earth</v-icon>
|
||||
<span v-if="item">{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.copyCount="{ item }">
|
||||
<v-chip v-if="activeTab === 'public'" size="small" color="indigo" variant="flat">{{ item.copyCount || 0 }}</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.popular="{ item }">
|
||||
<v-icon v-if="activeTab === 'public' && (item.copyCount || 0) > 10" color="amber">mdi-star</v-icon>
|
||||
</template>
|
||||
|
||||
<template #loading>
|
||||
<div class="pa-4">
|
||||
<v-skeleton-loader type="table" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<div class="text-center pa-8">
|
||||
<v-icon size="48" color="grey">mdi-file-search</v-icon>
|
||||
<div class="mt-2 mb-4">اطلاعاتی برای نمایش وجود ندارد</div>
|
||||
<v-btn color="primary" variant="tonal" @click="clearSearch">حذف جستجو</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table-server>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-eye"></i>
|
||||
<span>پیشنمایش قالبها</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-download"></i>
|
||||
<span>صدور قالبها</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-dialog v-model="confirmDeleteDialog" max-width="420">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">حذف قالب</v-card-title>
|
||||
<v-card-text>آیا از حذف این قالب مطمئن هستید؟</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="confirmDeleteDialog = false">انصراف</v-btn>
|
||||
<v-btn color="error" @click="confirmDelete">بله، حذف شود</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="showMessageDialog" max-width="360">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">{{ messageTitle }}</v-card-title>
|
||||
<v-card-text>{{ messageText }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" @click="showMessageDialog = false">باشه</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CustomInvoiceTemplates',
|
||||
data() {
|
||||
return {
|
||||
// Data will be added here
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
const searchValue = ref('')
|
||||
const items = ref([])
|
||||
const totalItems = ref(0)
|
||||
const activeTab = ref('business') // business | public
|
||||
|
||||
const serverOptions = ref({
|
||||
page: 1,
|
||||
rowsPerPage: 10,
|
||||
sortBy: [],
|
||||
})
|
||||
|
||||
const baseHeaders = [
|
||||
{ title: 'عملیات', key: 'operation', sortable: false, align: 'center' },
|
||||
{ title: 'نام قالب', key: 'name', sortable: true, align: 'start' },
|
||||
{ title: 'عمومی', key: 'isPublic', sortable: true, align: 'center' },
|
||||
{ title: 'سازنده', key: 'submitter', sortable: false, align: 'start' },
|
||||
]
|
||||
|
||||
const publicExtraHeaders = [
|
||||
{ title: 'تعداد کپی', key: 'copyCount', sortable: true, align: 'center' },
|
||||
{ title: 'محبوب', key: 'popular', sortable: false, align: 'center' },
|
||||
]
|
||||
|
||||
const computedHeaders = computed(() => {
|
||||
return activeTab.value === 'public' ? [...baseHeaders, ...publicExtraHeaders] : baseHeaders
|
||||
})
|
||||
|
||||
let debounceTimer
|
||||
|
||||
const confirmDeleteDialog = ref(false)
|
||||
const deleteTargetId = ref(null)
|
||||
|
||||
const showMessageDialog = ref(false)
|
||||
const messageTitle = ref('')
|
||||
const messageText = ref('')
|
||||
|
||||
const openMessage = (title, text) => {
|
||||
messageTitle.value = title
|
||||
messageText.value = text
|
||||
showMessageDialog.value = true
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = window.setTimeout(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const sortBy = serverOptions.value.sortBy.map((sort) => ({
|
||||
key: sort.key,
|
||||
order: sort.order === 'asc' ? 'ASC' : 'DESC',
|
||||
}))
|
||||
|
||||
const response = await axios.post('/api/plugins/custominvoice/template/list', {
|
||||
page: serverOptions.value.page,
|
||||
itemsPerPage: serverOptions.value.rowsPerPage,
|
||||
search: searchValue.value,
|
||||
sortBy: sortBy.length > 0 ? sortBy : null,
|
||||
scope: activeTab.value,
|
||||
})
|
||||
|
||||
items.value = response.data.items || []
|
||||
totalItems.value = response.data.total || 0
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error)
|
||||
items.value = []
|
||||
totalItems.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Methods will be added here
|
||||
},
|
||||
mounted() {
|
||||
if (this.$store) {
|
||||
this.$store.commit('setPageTitle', 'مدیریت قالبهای فاکتور')
|
||||
}, 400)
|
||||
}
|
||||
|
||||
const refresh = () => { fetchData() }
|
||||
const clearSearch = () => { searchValue.value = '' }
|
||||
|
||||
watch(searchValue, fetchData)
|
||||
watch(serverOptions, fetchData, { deep: true })
|
||||
watch(activeTab, () => {
|
||||
serverOptions.value.page = 1
|
||||
// default sort for public tab: by copyCount desc
|
||||
if (activeTab.value === 'public') {
|
||||
serverOptions.value.sortBy = [{ key: 'copyCount', order: 'desc' }]
|
||||
} else {
|
||||
serverOptions.value.sortBy = []
|
||||
}
|
||||
fetchData()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
const showRowActions = (item) => {
|
||||
return (activeTab.value === 'business' && item && item.ownedByMe) || activeTab.value === 'public'
|
||||
}
|
||||
|
||||
const copyAndEdit = async (id) => {
|
||||
try {
|
||||
const { data } = await axios.post(`/api/plugins/custominvoice/template/${id}/copy`)
|
||||
if (data && data.data && data.data.id) {
|
||||
router.push(`/acc/plugins/custominvoice/template/mod/${data.data.id}`)
|
||||
} else {
|
||||
throw new Error('invalid response')
|
||||
}
|
||||
} catch (e) {
|
||||
openMessage('خطا', 'کپی قالب با خطا مواجه شد')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteItem = (id) => {
|
||||
deleteTargetId.value = id
|
||||
confirmDeleteDialog.value = true
|
||||
}
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (!deleteTargetId.value) {
|
||||
confirmDeleteDialog.value = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
await axios.delete(`/api/plugins/custominvoice/template/${deleteTargetId.value}`)
|
||||
confirmDeleteDialog.value = false
|
||||
openMessage('حذف شد', 'قالب با موفقیت حذف شد')
|
||||
fetchData()
|
||||
} catch (e) {
|
||||
confirmDeleteDialog.value = false
|
||||
openMessage('خطا', 'حذف قالب با خطا مواجه شد')
|
||||
} finally {
|
||||
deleteTargetId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.templates-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
.custom-header {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.templates-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 30px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 15px;
|
||||
color: white;
|
||||
.custom-table .v-table__wrapper {
|
||||
max-height: 520px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.templates-header h1 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
.custom-table .v-data-table__th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.templates-description {
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.templates-content {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.templates-placeholder {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 4rem;
|
||||
color: #667eea;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.templates-placeholder h3 {
|
||||
font-size: 1.8rem;
|
||||
color: #333;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.templates-placeholder p {
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
margin: 0 0 40px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.placeholder-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
.name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
transition: all 0.3s ease;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.feature-item:hover {
|
||||
background: #e9ecef;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.feature-item i {
|
||||
font-size: 1.2rem;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.feature-item span {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.feature-item-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.feature-item-link:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.templates-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.templates-header {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.templates-header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.templates-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.templates-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.templates-placeholder {
|
||||
padding: 40px 15px;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.templates-placeholder h3 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.templates-placeholder p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.placeholder-features {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.templates-header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.templates-description {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.templates-placeholder h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.templates-placeholder p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.custom-table tbody tr:nth-child(odd) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
</style>
|
|
@ -65,6 +65,19 @@
|
|||
<v-tabs-window-item value="1">
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-row v-if="isPluginActive('custominvoice')">
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="settings.sell.templateId"
|
||||
:items="templateOptions"
|
||||
:loading="templatesLoading"
|
||||
clearable
|
||||
label="قالب سفارشی فاکتور فروش"
|
||||
hint="در صورت خالی بودن، از قالب پیشفرض سیستم استفاده میشود"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-row dense>
|
||||
|
@ -106,6 +119,19 @@
|
|||
<v-tabs-window-item value="2">
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-row v-if="isPluginActive('custominvoice')">
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="settings.buy.templateId"
|
||||
:items="templateOptions"
|
||||
:loading="templatesLoading"
|
||||
clearable
|
||||
label="قالب سفارشی فاکتور خرید"
|
||||
hint="در صورت خالی بودن، از قالب پیشفرض سیستم استفاده میشود"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-row dense>
|
||||
|
@ -141,6 +167,19 @@
|
|||
<v-tabs-window-item value="3">
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-row v-if="isPluginActive('custominvoice')">
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="settings.rfbuy.templateId"
|
||||
:items="templateOptions"
|
||||
:loading="templatesLoading"
|
||||
clearable
|
||||
label="قالب سفارشی برگشت از خرید"
|
||||
hint="در صورت خالی بودن، از قالب پیشفرض سیستم استفاده میشود"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-row dense>
|
||||
|
@ -176,6 +215,19 @@
|
|||
<v-tabs-window-item value="4">
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-row v-if="isPluginActive('custominvoice')">
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="settings.rfsell.templateId"
|
||||
:items="templateOptions"
|
||||
:loading="templatesLoading"
|
||||
clearable
|
||||
label="قالب سفارشی برگشت از فروش"
|
||||
hint="در صورت خالی بودن، از قالب پیشفرض سیستم استفاده میشود"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-row dense>
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue