more progress in openbalance and some new options

This commit is contained in:
Hesabix 2025-05-09 13:02:39 +00:00
parent 6b656ae0ac
commit e1e5c19112
434 changed files with 3592 additions and 1496 deletions

View file

@ -630,35 +630,6 @@ class AdminController extends AbstractController
throw $this->createNotFoundException();
}
/**
* @throws Exception
*/
#[Route('/api/admin/database/backup/create', name: 'app_admin_database_backup_create')]
public function app_admin_database_backup_create(KernelInterface $kernel): JsonResponse
{
$application = new Application($kernel);
$application->setAutoExit(false);
$input = new ArrayInput([
'command' => 'doctrine:schema:create',
// (optional) define the value of command arguments
'--dump-sql' => true,
]);
// You can use NullOutput() if you don't need the output
$output = new BufferedOutput();
$application->run($input, $output);
// return the output, don't use if you used NullOutput()
$content = $output->fetch();
$time = time();
$file = fopen(dirname(__DIR__, 3) . '/hesabixBackup/versions/Hesabix-' . $time . '.sql', 'w');
fwrite($file, $content);
fclose($file);
return $this->json([
'result' => 0,
'filename' => 'Hesabix-' . $time . '.sql',
]);
}
#[Route('/api/admin/logs/last', name: 'api_admin_logs_last')]
public function api_admin_logs_last(Extractor $extractor, Jdate $jdate, EntityManagerInterface $entityManager): JsonResponse
{

View file

@ -145,10 +145,16 @@ class BuyController extends AbstractController
$hesabdariRow->setBd(0);
$hesabdariRow->setBs($params['discountAll']);
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
'code' => '51' // تخفیفات نقدی خرید
'code' => '51'
]);
$hesabdariRow->setRef($ref);
$entityManager->persist($hesabdariRow);
// ذخیره نوع تخفیف و درصد آن
$doc->setDiscountType($params['discountType'] ?? 'fixed');
if (isset($params['discountPercent'])) {
$doc->setDiscountPercent((float)$params['discountPercent']);
}
}
$doc->setDes($params['des']);
$doc->setDate($params['date']);

View file

@ -139,8 +139,11 @@ class CommodityController extends AbstractController
]);
$count = 0;
foreach ($rows as $row) {
$count += $row->getDoc()->getType() === 'buy' ? $row->getCommdityCount() : -$row->getCommdityCount();
}
if ($row->getDoc()->getType() === 'buy' || $row->getDoc()->getType() === 'open_balance') {
$count += $row->getCommdityCount();
} else {
$count -= $row->getCommdityCount();
} }
$temp['count'] = $count;
}
return $temp;

View file

@ -8,6 +8,7 @@ use App\Entity\HesabdariDoc;
use App\Entity\HesabdariTable;
use App\Entity\Person;
use App\Entity\Salary;
use App\Entity\Commodity;
use App\Entity\Shareholder;
use App\Service\Access;
use App\Service\Explore;
@ -113,6 +114,18 @@ class OpenbalanceController extends AbstractController
}
$res['shareholders'] = $shareholderDet;
//load commodities
foreach ($doc->getHesabdariRows() as $row) {
if ($row->getCommodity()) {
$temp = [];
$temp['info'] = Explore::ExploreCommodity($row->getCommodity());
$temp['count'] = $row->getCommdityCount();
$temp['price'] = $row->getBs()/$row->getCommdityCount();
$temp['totalPrice'] = $row->getBs();
$res['commodities'][] = $temp;
}
}
return $this->json($extractor->operationSuccess($res));
}
@ -425,4 +438,99 @@ class OpenbalanceController extends AbstractController
$entityManagerInterface->flush();
return $this->json($extractor->operationSuccess());
}
#[Route('/api/openbalance/save/commodities', name: 'app_openbalance_save_commodity')]
public function app_openbalance_save_commodity(Provider $provider,Jdate $jdate, Request $request, Access $access, EntityManagerInterface $entityManagerInterface, Extractor $extractor): Response
{
$acc = $access->hasRole('accounting');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
//get open balance doc
$doc = $entityManagerInterface->getRepository(HesabdariDoc::class)->findOneBy([
'year' => $acc['year'],
'bid' => $acc['bid'],
'type' => 'open_balance',
'money' => $acc['money']
]);
if (!$doc) {
$doc = new HesabdariDoc();
$doc->setBid($acc['bid']);
$doc->setAmount(0);
$doc->setDateSubmit(time());
$doc->setMoney($acc['money']);
$doc->setSubmitter($this->getUser());
$doc->setYear($acc['year']);
$doc->setDes('سند افتتاحیه');
$doc->setDate($jdate->jdate('Y/n/d', time()));
$doc->setType('open_balance');
$doc->setCode($provider->getAccountingCode($acc['bid'],'accounting'));
$entityManagerInterface->persist($doc);
}
// ایجاد آرایه از کدهای کالاهای ارسالی
$submittedCommodityCodes = array_map(function($param) {
return $param['info']['code'];
}, $params);
// حذف سطرهای مربوط به کالاهایی که در لیست ارسالی نیستند
foreach ($doc->getHesabdariRows() as $row) {
if ($row->getCommodity() && $row->getRefData() == 'commodity') {
$commodityCode = $row->getCommodity()->getCode();
if (!in_array($commodityCode, $submittedCommodityCodes)) {
$doc->removeHesabdariRow($row);
$entityManagerInterface->remove($row);
}
}
}
foreach ($params as $param) {
$commodity = $entityManagerInterface->getRepository(Commodity::class)->findOneBy([
'code' => $param['info']['code'],
'bid' => $acc['bid'],
]);
if(!$commodity) return $this->json($extractor->operationFail());
$ExistBefore = false;
foreach ($doc->getHesabdariRows() as $row) {
if ($row->getCommodity() == $commodity) {
if ($param['count'] != 0) {
$ExistBefore = true;
$row->setCommdityCount($param['count']);
$row->setBs($param['price'] * $param['count']);
$entityManagerInterface->persist($row);
}
}
}
if ((!$ExistBefore) && $param['count'] != 0) {
$row = new HesabdariRow();
$row->setDoc($doc);
$row->setCommodity($commodity);
$row->setCommdityCount($param['count']);
$row->setBs(0);
$row->setBd($param['price'] * $param['count']);
$row->setRefData('commodity');
$row->setBid($acc['bid']);
$row->setYear($acc['year']);
$row->setDes('موجودی اول دوره');
$row->setRef($entityManagerInterface->getRepository(HesabdariTable::class)->findOneBy(['code' => 120]));
$entityManagerInterface->persist($row);
}
}
//calculate amount of document
foreach ($doc->getHesabdariRows() as $row) {
$doc->setAmount($doc->getAmount() + $row->getBd());
}
$entityManagerInterface->persist($doc);
$entityManagerInterface->flush();
return $this->json($extractor->operationSuccess());
}
}

View file

@ -419,4 +419,137 @@ class ReportController extends AbstractController
return $this->json(['error' => 'An error occurred: ' . $e->getMessage()], 500);
}
}
#[Route('/api/report/top-selling-commodities-by-price', name: 'app_report_top_selling_commodities_by_price', methods: ['POST'])]
public function app_report_top_selling_commodities_by_price(Access $access, Explore $explore, Jdate $jdate, Request $request, EntityManagerInterface $entityManager, LoggerInterface $logger): JsonResponse
{
$acc = $access->hasRole('report');
if (!$acc) {
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم برای مشاهده این اطلاعات را ندارید.');
}
}
/** @var Business $business */
$business = $acc['bid'];
/** @var Year $year */
$year = $acc['year'];
$payload = $request->getPayload();
$period = $payload->get('period', 'year');
$limit = (int) $payload->get('limit', 10);
if ($limit < 3) {
$limit = 3;
}
$today = $jdate->GetTodayDate();
list($currentYear, $currentMonth, $currentDay) = explode('/', $today);
switch ($period) {
case 'today':
$dateStart = $today;
$dateEnd = $today;
break;
case 'week':
$weekDay = (int) $jdate->jdate('w', time());
$daysToSubtract = $weekDay;
$dateStart = $jdate->shamsiDate(0, 0, -$daysToSubtract);
$dateEnd = $jdate->shamsiDate(0, 0, 6 - $weekDay);
break;
case 'month':
$dateStart = "$currentYear/$currentMonth/01";
$dateEnd = "$currentYear/$currentMonth/" . $jdate->jdate('t', $jdate->jallaliToUnixTime("$currentYear/$currentMonth/01"));
break;
case 'year':
default:
$dateStart = $jdate->jdate('Y/m/d', $year->getStart());
$dateEnd = $jdate->jdate('Y/m/d', $year->getEnd());
break;
}
$queryBuilder = $entityManager->createQueryBuilder();
$queryBuilder
->select('c.id AS id')
->addSelect('c.code AS code')
->addSelect('c.name AS name')
->addSelect('c.des AS des')
->addSelect('c.priceBuy AS priceBuy')
->addSelect('c.priceSell AS priceSell')
->addSelect('c.khadamat AS khadamat')
->addSelect('c.orderPoint AS orderPoint')
->addSelect('c.commodityCountCheck AS commodityCountCheck')
->addSelect('c.minOrderCount AS minOrderCount')
->addSelect('c.dayLoading AS dayLoading')
->addSelect('c.speedAccess AS speedAccess')
->addSelect('c.withoutTax AS withoutTax')
->addSelect('c.barcodes AS barcodes')
->addSelect('IDENTITY(c.unit) AS unitId')
->addSelect('u.name AS unit')
->addSelect('SUM(CAST(hr.commdityCount AS INTEGER)) AS totalCount')
->addSelect('SUM(hr.bs) AS totalPrice') // محاسبه مجموع قیمت فروش با استفاده از فیلد bs
->from(HesabdariRow::class, 'hr')
->innerJoin('hr.doc', 'hd')
->innerJoin('hr.commodity', 'c')
->leftJoin('c.unit', 'u')
->where('hd.bid = :business')
->andWhere('hd.type = :type')
->andWhere('hr.year = :year')
->andWhere('hd.date BETWEEN :dateStart AND :dateEnd')
->setParameter('business', $business)
->setParameter('type', 'sell')
->setParameter('year', $year)
->setParameter('dateStart', $dateStart)
->setParameter('dateEnd', $dateEnd)
->groupBy('c.id')
->addGroupBy('u.name')
->orderBy('totalPrice', 'DESC') // مرتب‌سازی بر اساس مجموع قیمت فروش
->setMaxResults($limit);
try {
$results = $queryBuilder->getQuery()->getArrayResult();
$logger->info('Query executed successfully', [
'sql' => $queryBuilder->getQuery()->getSQL(),
'params' => $queryBuilder->getQuery()->getParameters()->toArray(),
'results' => $results
]);
if (empty($results)) {
$logger->info('No results returned from query');
return $this->json(['message' => 'No data found'], 200);
}
$topCommodities = array_map(function ($result) {
return [
'id' => $result['id'],
'code' => $result['code'],
'name' => $result['name'],
'des' => $result['des'],
'priceBuy' => $result['priceBuy'],
'priceSell' => $result['priceSell'],
'khadamat' => $result['khadamat'],
'orderPoint' => $result['orderPoint'],
'commodityCountCheck' => $result['commodityCountCheck'],
'minOrderCount' => $result['minOrderCount'],
'dayLoading' => $result['dayLoading'],
'speedAccess' => $result['speedAccess'],
'withoutTax' => $result['withoutTax'],
'barcodes' => $result['barcodes'],
'unit' => $result['unit'] ?? '',
'count' => (int) $result['totalCount'],
'totalPrice' => (float) $result['totalPrice'] // مجموع قیمت فروش
];
}, $results);
return $this->json($topCommodities);
} catch (\Exception $e) {
$logger->error('Error in top-selling commodities by price query', [
'message' => $e->getMessage(),
'sql' => $queryBuilder->getQuery()->getSQL(),
'params' => $queryBuilder->getQuery()->getParameters()->toArray(),
'trace' => $e->getTraceAsString()
]);
return $this->json(['error' => 'An error occurred: ' . $e->getMessage()], 500);
}
}
}

View file

@ -146,10 +146,16 @@ class RfsellController extends AbstractController
$hesabdariRow->setBd(0);
$hesabdariRow->setBs($params['discountAll']);
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
'code' => '104' // سایر هزینه های پخش و خرید
'code' => '104'
]);
$hesabdariRow->setRef($ref);
$entityManager->persist($hesabdariRow);
// ذخیره نوع تخفیف و درصد آن
$doc->setDiscountType($params['discountType'] ?? 'fixed');
if (isset($params['discountPercent'])) {
$doc->setDiscountPercent((float)$params['discountPercent']);
}
}
$doc->setDes($params['des']);
$doc->setDate($params['date']);

View file

@ -27,6 +27,9 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Entity\BankAccount;
use App\Entity\Cashdesk;
use App\Entity\Salary;
class SellController extends AbstractController
{
@ -72,7 +75,7 @@ class SellController extends AbstractController
foreach ($doc->getHesabdariRows() as $item) {
if ($item->getCommodity() && $item->getCommdityCount()) {
if ($acc['bid']->getProfitCalctype() == 'simple') {
$profit = $profit + (($item->getCommodity()->getPriceSell() - $item->getCommodity()->getPriceSell()) * $item->getCommdityCount());
$profit = $profit + (($item->getCommodity()->getPriceSell() - $item->getCommodity()->getPriceBuy()) * $item->getCommdityCount());
} elseif ($acc['bid']->getProfitCalctype() == 'lis') {
$last = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'commodity' => $item->getCommodity(),
@ -82,7 +85,7 @@ class SellController extends AbstractController
]);
if ($last) {
$price = $last->getBd() / $last->getCommdityCount();
$profit = $profit + ((($item->getBs() / $item->getCommdityCount()) - $price) * $item->getCommdityCount());
$profit = $profit + (($item->getBs() / $item->getCommdityCount() - $price) * $item->getCommdityCount());
} else {
$profit = $profit + $item->getBs();
}
@ -101,7 +104,7 @@ class SellController extends AbstractController
}
if ($count != 0) {
$price = $avg / $count;
$profit = $profit + ((($item->getBs() / $item->getCommdityCount()) - $price) * $item->getCommdityCount());
$profit = $profit + (($item->getBs() / $item->getCommdityCount() - $price) * $item->getCommdityCount());
} else {
$profit = $profit + $item->getBs();
}
@ -195,6 +198,12 @@ class SellController extends AbstractController
]);
$hesabdariRow->setRef($ref);
$entityManager->persist($hesabdariRow);
// ذخیره نوع تخفیف و درصد آن
$doc->setDiscountType($params['discountType'] ?? 'fixed');
if (isset($params['discountPercent'])) {
$doc->setDiscountPercent((float)$params['discountPercent']);
}
}
$doc->setDes($params['des']);
$doc->setDate($params['date']);
@ -264,10 +273,9 @@ class SellController extends AbstractController
$pair = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $pairCode,
'type' => 'buy'
]);
if ($pair) {
$doc->addPairDoc($pair);
$pair->addRelatedDoc($doc);
}
}
}
@ -577,7 +585,6 @@ class SellController extends AbstractController
]);
}
// متد calculateProfit بدون تغییر
private function calculateProfit(int $docId, array $acc, EntityManagerInterface $entityManager): int
{
$profit = 0;
@ -591,7 +598,7 @@ class SellController extends AbstractController
->findOneBy(['commodity' => $commodityId, 'bs' => 0], ['id' => 'DESC']);
if ($last) {
$price = $last->getBd() / $last->getCommdityCount();
$profit += ((($item->getBs() / $item->getCommdityCount()) - $price) * $item->getCommdityCount());
$profit += ($item->getBs() / $item->getCommdityCount() - $price) * $item->getCommdityCount();
} else {
$profit += $item->getBs();
}
@ -600,7 +607,7 @@ class SellController extends AbstractController
}
} elseif ($acc['bid']->getProfitCalctype() === 'simple') {
if ($item->getCommodity() && $item->getCommodity()->getPriceSell() !== null && $item->getCommodity()->getPriceBuy() !== null) {
$profit += (($item->getCommodity()->getPriceSell() - $item->getCommodity()->getPriceBuy()) * $item->getCommdityCount());
$profit += ($item->getCommodity()->getPriceSell() - $item->getCommodity()->getPriceBuy()) * $item->getCommdityCount();
} else {
$profit += $item->getBs();
}
@ -608,11 +615,15 @@ class SellController extends AbstractController
if ($commodityId) {
$lasts = $entityManager->getRepository(HesabdariRow::class)
->findBy(['commodity' => $commodityId, 'bs' => 0], ['id' => 'DESC']);
$avg = array_sum(array_map(fn($last) => $last->getBd(), $lasts));
$count = array_sum(array_map(fn($last) => $last->getCommdityCount(), $lasts));
$avg = 0;
$count = 0;
foreach ($lasts as $last) {
$avg += $last->getBd();
$count += $last->getCommdityCount();
}
if ($count != 0) {
$price = $avg / $count;
$profit += ((($item->getBs() / $item->getCommdityCount()) - $price) * $item->getCommdityCount());
$profit += ($item->getBs() / $item->getCommdityCount() - $price) * $item->getCommdityCount();
} else {
$profit += $item->getBs();
}
@ -683,19 +694,27 @@ class SellController extends AbstractController
throw $this->createAccessDeniedException();
$params = json_decode($request->getContent(), true);
$printOptions = $params['printOptions'] ?? [];
$params['printers'] = $params['printers'] ?? false;
$params['pdf'] = $params['pdf'] ?? true;
$params['posPrint'] = $params['posPrint'] ?? false;
// اضافه کردن کلیدهای پیش‌فرض
$printOptions = array_merge([
'note' => true,
'bidInfo' => true,
'taxInfo' => true,
'discountInfo' => true,
'pays' => false,
'paper' => 'A4-L',
'invoiceIndex' => false,
'businessStamp' => false
], $printOptions);
// دریافت تنظیمات پیش‌فرض از PrintOptions
$printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid' => $acc['bid']]);
// تنظیم مقادیر پیش‌فرض از تنظیمات ذخیره شده
$defaultOptions = [
'note' => $printSettings ? $printSettings->isSellNote() : true,
'bidInfo' => $printSettings ? $printSettings->isSellBidInfo() : true,
'taxInfo' => $printSettings ? $printSettings->isSellTaxInfo() : true,
'discountInfo' => $printSettings ? $printSettings->isSellDiscountInfo() : true,
'pays' => $printSettings ? $printSettings->isSellPays() : true,
'paper' => $printSettings ? $printSettings->getSellPaper() : 'A4-L',
'invoiceIndex' => $printSettings ? $printSettings->isSellInvoiceIndex() : true,
'businessStamp' => $printSettings ? $printSettings->isSellBusinessStamp() : true
];
// اولویت با پارامترهای ارسالی است
$printOptions = array_merge($defaultOptions, $params['printOptions'] ?? []);
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
@ -717,43 +736,8 @@ class SellController 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'];
}
if (array_key_exists('invoiceIndex', $params['printOptions'])) {
$printOptions['invoiceIndex'] = $params['printOptions']['invoiceIndex'];
}
if (array_key_exists('businessStamp', $params['printOptions'])) {
$printOptions['businessStamp'] = $params['printOptions']['businessStamp'];
}
}
if ($params['pdf'] == true || $params['printers'] == true) {
$note = '';
$printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid' => $acc['bid']]);
if ($printSettings) {
$note = $printSettings->getSellNoteString();
}
@ -775,7 +759,8 @@ class SellController extends AbstractController
$printOptions['paper']
);
}
if ($params['printers'] == true) {
if ($params['posPrint'] == true) {
$pid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
@ -831,4 +816,473 @@ class SellController extends AbstractController
'daySells' => $daySells
]);
}
#[Route('/api/sell/v2/mod', name: 'app_sell_v2_mod', methods: ['POST'])]
public function app_sell_v2_mod(
AccountingPermissionService $accountingPermissionService,
PluginService $pluginService,
SMS $SMS,
Provider $provider,
Extractor $extractor,
Request $request,
Access $access,
Log $log,
EntityManagerInterface $entityManager,
registryMGR $registryMGR
): JsonResponse {
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$pkgcntr = $accountingPermissionService->canRegisterAccountingDoc($acc['bid']);
if ($pkgcntr['code'] == 4) {
return $this->json([
'result' => 4,
'message' => $pkgcntr['message']
]);
}
try {
// بررسی وجود فاکتور برای ویرایش
if (!empty($params['id'])) {
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['id'],
'money' => $acc['money']
]);
if (!$doc) {
return $this->json($extractor->notFound());
}
// حذف سطرهای قبلی
$rows = $doc->getHesabdariRows();
foreach ($rows as $row) {
$entityManager->remove($row);
}
} else {
// ایجاد فاکتور جدید
$doc = new HesabdariDoc();
$doc->setBid($acc['bid']);
$doc->setYear($acc['year']);
$doc->setDateSubmit(time());
$doc->setType('sell');
$doc->setSubmitter($this->getUser());
$doc->setMoney($acc['money']);
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
}
// تنظیم اطلاعات اصلی فاکتور
$doc->setDes($params['invoiceDescription']);
$doc->setDate($params['invoiceDate']);
$doc->setTaxPercent($params['taxPercent'] ?? 0);
// افزودن هزینه حمل
if ($params['shippingCost'] > 0) {
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDes('حمل و نقل کالا');
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs($params['shippingCost']);
$hesabdariRow->setBd(0);
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '61']);
$hesabdariRow->setRef($ref);
$entityManager->persist($hesabdariRow);
}
// افزودن تخفیف کلی
$totalDiscount = 0;
if ($params['discountType'] === 'percent') {
$totalDiscount = round(($params['totalInvoice'] * $params['discountPercent']) / 100);
$doc->setDiscountType('percent');
$doc->setDiscountPercent((float)$params['discountPercent']);
} else {
$totalDiscount = $params['totalDiscount'];
$doc->setDiscountType('fixed');
$doc->setDiscountPercent(null);
}
if ($totalDiscount > 0) {
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDes('تخفیف فاکتور');
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs(0);
$hesabdariRow->setBd($totalDiscount);
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '104']);
$hesabdariRow->setRef($ref);
$entityManager->persist($hesabdariRow);
}
// افزودن اقلام فاکتور
$sumTax = 0;
$sumTotal = 0;
foreach ($params['items'] as $item) {
$sumTax += $item['tax'] ?? 0;
$sumTotal += $item['total'] ?? 0;
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDes($item['description'] ?? '');
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs($item['total'] + ($item['tax'] ?? 0));
$hesabdariRow->setBd(0);
$hesabdariRow->setDiscount($item['discountAmount'] ?? 0);
$hesabdariRow->setTax($item['tax'] ?? 0);
$hesabdariRow->setDiscountType($item['showPercentDiscount'] ? 'percent' : 'fixed');
$hesabdariRow->setDiscountPercent($item['discountPercent'] ?? 0);
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '53']);
$hesabdariRow->setRef($ref);
$commodity = $entityManager->getRepository(Commodity::class)->findOneBy([
'id' => $item['name']['id'],
'bid' => $acc['bid']
]);
if (!$commodity) {
throw new \Exception('کالا یافت نشد');
}
$hesabdariRow->setCommodity($commodity);
$hesabdariRow->setCommdityCount($item['count']);
// به‌روزرسانی قیمت فروش کالا اگر تنظیم شده باشد
if ($acc['bid']->isCommodityUpdateSellPriceAuto() && $commodity->getPriceSell() != $item['price']) {
$commodity->setPriceSell($item['price']);
$entityManager->persist($commodity);
}
$entityManager->persist($hesabdariRow);
}
// افزودن ردیف مالیات
if ($sumTax > 0) {
$taxRow = new HesabdariRow();
$taxRow->setDes('مالیات بر ارزش افزوده');
$taxRow->setBid($acc['bid']);
$taxRow->setYear($acc['year']);
$taxRow->setDoc($doc);
$taxRow->setBs($sumTax);
$taxRow->setBd(0);
$taxRef = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '33']);
$taxRow->setRef($taxRef);
$entityManager->persist($taxRow);
}
// تنظیم مبلغ کل فاکتور
$doc->setAmount($sumTotal + $sumTax - $totalDiscount + $params['shippingCost']);
// افزودن سطر اصلی فاکتور
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDes('فاکتور فروش');
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs(0);
$hesabdariRow->setBd($sumTotal + $sumTax + $params['shippingCost'] - $totalDiscount);
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '3']);
$hesabdariRow->setRef($ref);
$person = $entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'id' => $params['customer']
]);
if (!$person) {
throw new \Exception('خریدار یافت نشد');
}
$hesabdariRow->setPerson($person);
$entityManager->persist($hesabdariRow);
// ذخیره فاکتور
$entityManager->persist($doc);
$entityManager->flush();
// ایجاد لینک کوتاه اگر وجود نداشته باشد
if (!$doc->getShortlink()) {
$doc->setShortlink($provider->RandomString(8));
$entityManager->persist($doc);
$entityManager->flush();
}
// ثبت اسناد پرداخت
if (!empty($params['payments'])) {
foreach ($params['payments'] as $payment) {
// ایجاد سند حسابداری جدید برای پرداخت
$paymentDoc = new HesabdariDoc();
$paymentDoc->setBid($acc['bid']);
$paymentDoc->setYear($acc['year']);
$paymentDoc->setDateSubmit(time());
$paymentDoc->setType('sell_receive');
$paymentDoc->setSubmitter($this->getUser());
$paymentDoc->setMoney($acc['money']);
$paymentDoc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
$paymentDoc->setDate($params['invoiceDate']);
$paymentDoc->setDes($payment['description'] ?? 'دریافت وجه فاکتور فروش شماره ' . $doc->getCode());
$paymentDoc->setAmount($payment['amount']);
// ایجاد ارتباط با فاکتور اصلی
$doc->addRelatedDoc($paymentDoc);
// ایجاد سطرهای حسابداری بر اساس نوع پرداخت
if ($payment['type'] === 'bank') {
// دریافت از طریق حساب بانکی
$bankRow = new HesabdariRow();
$bankRow->setDes($payment['description'] ?? 'دریافت وجه فاکتور فروش شماره ' . $doc->getCode());
$bankRow->setBid($acc['bid']);
$bankRow->setYear($acc['year']);
$bankRow->setDoc($paymentDoc);
$bankRow->setBs(0);
$bankRow->setBd($payment['amount']);
$bankRef = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '5']);
$bankRow->setRef($bankRef);
$bankRow->setBank($entityManager->getRepository(BankAccount::class)->find($payment['bank']));
$entityManager->persist($bankRow);
} elseif ($payment['type'] === 'cashdesk') {
// دریافت از طریق صندوق
$cashdeskRow = new HesabdariRow();
$cashdeskRow->setDes($payment['description'] ?? 'دریافت وجه فاکتور فروش شماره ' . $doc->getCode());
$cashdeskRow->setBid($acc['bid']);
$cashdeskRow->setYear($acc['year']);
$cashdeskRow->setDoc($paymentDoc);
$cashdeskRow->setBs(0);
$cashdeskRow->setBd($payment['amount']);
$cashdeskRef = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '121']);
$cashdeskRow->setRef($cashdeskRef);
$cashdeskRow->setCashdesk($entityManager->getRepository(Cashdesk::class)->find($payment['cashdesk']));
$entityManager->persist($cashdeskRow);
} elseif ($payment['type'] === 'salary') {
// دریافت از طریق تنخواه گردان
$salaryRow = new HesabdariRow();
$salaryRow->setDes($payment['description'] ?? 'دریافت وجه فاکتور فروش شماره ' . $doc->getCode());
$salaryRow->setBid($acc['bid']);
$salaryRow->setYear($acc['year']);
$salaryRow->setDoc($paymentDoc);
$salaryRow->setBs(0);
$salaryRow->setBd($payment['amount']);
$salaryRef = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '122']);
$salaryRow->setRef($salaryRef);
$salaryRow->setSalary($entityManager->getRepository(Salary::class)->find($payment['salary']));
$entityManager->persist($salaryRow);
}
// ایجاد سطر دریافت از مشتری
$receiveRow = new HesabdariRow();
$receiveRow->setDes($payment['description'] ?? 'پرداخت وجه فاکتور فروش شماره ' . $doc->getCode());
$receiveRow->setBid($acc['bid']);
$receiveRow->setYear($acc['year']);
$receiveRow->setDoc($paymentDoc);
$receiveRow->setBs($payment['amount']);
$receiveRow->setBd(0);
$receiveRef = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '3']);
$receiveRow->setRef($receiveRef);
$receiveRow->setPerson($person);
$entityManager->persist($receiveRow);
$entityManager->persist($paymentDoc);
}
$entityManager->flush();
}
// ثبت لاگ
$log->insert(
'حسابداری',
'سند حسابداری شماره ' . $doc->getCode() . ' ثبت / ویرایش شد.',
$this->getUser(),
$request->headers->get('activeBid'),
$doc
);
// ارسال پیامک اگر درخواست شده باشد
if (!empty($params['sendSmsToCustomer']) && $params['sendSmsToCustomer']) {
if ($pluginService->isActive('accpro', $acc['bid']) && $person->getMobile() != '' && $acc['bid']->getTel()) {
$SMS->sendByBalance(
[$person->getnikename(), 'sell/' . $acc['bid']->getId() . '/' . $doc->getShortlink(), $acc['bid']->getName(), $acc['bid']->getTel()],
$registryMGR->get('sms', 'plugAccproSharefaktor'),
$person->getMobile(),
$acc['bid'],
$this->getUser(),
3
);
} else {
$SMS->sendByBalance(
[$acc['bid']->getName(), 'sell/' . $acc['bid']->getId() . '/' . $doc->getShortlink()],
$registryMGR->get('sms', 'sharefaktor'),
$person->getMobile(),
$acc['bid'],
$this->getUser(),
3
);
}
}
return $this->json([
'result' => 1,
'message' => 'فاکتور با موفقیت ثبت شد',
'data' => [
'id' => $doc->getCode(),
'code' => $doc->getCode(),
'shortlink' => $doc->getShortlink()
]
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => $e->getMessage()
]);
}
}
#[Route('/api/sell/v2/get/{id}', name: 'app_sell_v2_get', methods: ['GET'])]
public function app_sell_v2_get(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
string $id
): JsonResponse {
try {
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $id,
'money' => $acc['money']
]);
if (!$doc) {
throw $this->createNotFoundException('فاکتور یافت نشد');
}
$person = null;
$discountAll = 0;
$transferCost = 0;
$items = [];
$totalInvoice = 0;
$taxPercent = $doc->getTaxPercent();
$discountType = $doc->getDiscountType() ?? 'fixed';
$discountPercent = $doc->getDiscountPercent() ?? 0;
$payments = [];
// دریافت اسناد پرداخت مرتبط
$relatedDocs = $doc->getRelatedDocs();
foreach ($relatedDocs as $relatedDoc) {
if ($relatedDoc->getType() === 'sell_receive') {
$payment = [
'type' => null,
'amount' => $relatedDoc->getAmount(),
'reference' => '',
'description' => $relatedDoc->getDes(),
'bank' => null,
'cashdesk' => null,
'salary' => null
];
foreach ($relatedDoc->getHesabdariRows() as $row) {
if ($row->getBank()) {
$payment['type'] = 'bank';
$payment['bank'] = $row->getBank()->getId();
} elseif ($row->getCashdesk()) {
$payment['type'] = 'cashdesk';
$payment['cashdesk'] = $row->getCashdesk()->getId();
} elseif ($row->getSalary()) {
$payment['type'] = 'salary';
$payment['salary'] = $row->getSalary()->getId();
}
}
$payments[] = $payment;
}
}
foreach ($doc->getHesabdariRows() as $row) {
if ($row->getPerson()) {
$person = $row->getPerson();
} elseif ($row->getRef() && $row->getRef()->getCode() == '104') {
$discountAll = $row->getBd();
} elseif ($row->getRef() && $row->getRef()->getCode() == '61') {
$transferCost = $row->getBs();
} elseif ($row->getCommodity()) {
$basePrice = $row->getBs();
$itemDiscount = $row->getDiscount() ?? 0;
$itemDiscountType = $row->getDiscountType() ?? 'fixed';
$itemDiscountPercent = $row->getDiscountPercent() ?? 0;
// محاسبه تخفیف سطری
if ($itemDiscountType === 'percent') {
$itemDiscount = round(($basePrice * $itemDiscountPercent) / 100);
}
$itemTotal = $basePrice - $itemDiscount;
$totalInvoice += $itemTotal;
$items[] = [
'name' => [
'id' => $row->getCommodity()->getId(),
'name' => $row->getCommodity()->getName(),
'code' => $row->getCommodity()->getCode()
],
'count' => $row->getCommdityCount(),
'price' => $row->getCommdityCount() > 0 ? $basePrice / $row->getCommdityCount() : 0,
'discountPercent' => $itemDiscountPercent,
'discountAmount' => $itemDiscount,
'total' => $itemTotal,
'description' => $row->getDes(),
'showPercentDiscount' => $itemDiscountType === 'percent',
'tax' => $row->getTax() ?? 0
];
}
}
// محاسبه تخفیف کلی از HesabdariDoc
$totalDiscount = 0;
if ($discountType === 'percent') {
$totalDiscount = round(($totalInvoice * $discountPercent) / 100);
} else {
$totalDiscount = $discountAll;
}
return $this->json([
'result' => 1,
'data' => [
'id' => $doc->getCode(),
'date' => $doc->getDate(),
'person' => $person ? [
'id' => $person->getId(),
'name' => $person->getNikename(),
'code' => $person->getCode()
] : null,
'des' => $doc->getDes(),
'totalInvoice' => $totalInvoice,
'taxPercent' => $taxPercent,
'discountType' => $discountType,
'discountPercent' => $discountPercent,
'totalDiscount' => $totalDiscount,
'shippingCost' => $transferCost,
'showTotalPercentDiscount' => $discountType === 'percent',
'items' => $items,
'finalTotal' => $doc->getAmount(),
'payments' => $payments
]
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => $e->getMessage()
]);
}
}
}

View file

@ -0,0 +1,255 @@
<?php
namespace App\Controller\System;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\registryMGR;
final class DatabaseController extends AbstractController
{
private string $backupPath;
private registryMGR $registryMGR;
private EntityManagerInterface $entityManager;
public function __construct(registryMGR $registryMGR, EntityManagerInterface $entityManager)
{
$this->registryMGR = $registryMGR;
$this->entityManager = $entityManager;
$this->backupPath = dirname(__DIR__, 2) . '/hesabixBackup/versions';
}
#[Route('/api/admin/database/backup/info', name: 'app_admin_database_backup_info', methods: ['GET'])]
public function getBackupInfo(): JsonResponse
{
try {
$lastBackup = $this->getLastBackupInfo('local');
$lastFtpBackup = $this->getLastBackupInfo('ftp');
return $this->json([
'result' => 1,
'lastBackup' => $lastBackup,
'lastFtpBackup' => $lastFtpBackup
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => 'خطا در دریافت اطلاعات پشتیبان: ' . $e->getMessage()
], 500);
}
}
#[Route('/api/admin/database/backup/create', name: 'app_admin_database_backup_create', methods: ['POST'])]
public function app_admin_database_backup_create(): JsonResponse
{
try {
// ایجاد پوشه‌های مورد نیاز
$this->ensureBackupDirectoriesExist();
// ایجاد نام فایل با timestamp
$filename = 'Hesabix-' . time() . '.sql';
$filepath = $this->backupPath . '/' . $filename;
// دریافت تنظیمات دیتابیس از EntityManager
$connection = $this->entityManager->getConnection();
$params = $connection->getParams();
$dbName = $params['dbname'];
$dbUser = $params['user'];
$dbPass = $params['password'];
$dbHost = $params['host'];
$dbPort = $params['port'] ?? '3306';
// دستور mysqldump
$command = sprintf(
'mysqldump -h %s -P %s -u %s -p%s %s > %s',
escapeshellarg($dbHost),
escapeshellarg($dbPort),
escapeshellarg($dbUser),
escapeshellarg($dbPass),
escapeshellarg($dbName),
escapeshellarg($filepath)
);
// اجرای دستور
exec($command, $output, $returnVar);
if ($returnVar !== 0) {
throw new \Exception('خطا در اجرای دستور mysqldump: ' . implode("\n", $output));
}
// ذخیره اطلاعات آخرین پشتیبان
$this->updateLastBackupInfo('local', $filename);
return $this->json([
'result' => 1,
'filename' => $filename,
'message' => 'پشتیبان با موفقیت ایجاد شد'
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => 'خطا در ایجاد پشتیبان: ' . $e->getMessage()
], 500);
}
}
#[Route('/api/admin/database/backup/create-and-upload', name: 'app_admin_database_backup_create_and_upload', methods: ['POST'])]
public function createAndUploadToFtp(): JsonResponse
{
try {
// ایجاد پشتیبان محلی
$backupResponse = $this->app_admin_database_backup_create();
$backupData = json_decode($backupResponse->getContent(), true);
if ($backupData['result'] !== 1) {
throw new \Exception($backupData['message']);
}
$filename = $backupData['filename'];
$filepath = $this->backupPath . '/' . $filename;
// بررسی وجود فایل و دسترسی‌های آن
if (!file_exists($filepath)) {
throw new \Exception("فایل پشتیبان در مسیر {$filepath} یافت نشد");
}
if (!is_readable($filepath)) {
throw new \Exception("عدم دسترسی به فایل پشتیبان در مسیر {$filepath}");
}
// دریافت تنظیمات FTP
$ftpEnabled = filter_var($this->registryMGR->get('system_settings', 'ftp_enabled'), FILTER_VALIDATE_BOOLEAN);
if (!$ftpEnabled) {
throw new \Exception('اتصال FTP غیرفعال است');
}
$ftpHost = $this->registryMGR->get('system_settings', 'ftp_host');
$ftpPort = $this->registryMGR->get('system_settings', 'ftp_port');
$ftpUsername = $this->registryMGR->get('system_settings', 'ftp_username');
$ftpPassword = $this->registryMGR->get('system_settings', 'ftp_password');
$ftpPath = $this->registryMGR->get('system_settings', 'ftp_path');
// اتصال به FTP
$ftp = ftp_connect($ftpHost, (int)$ftpPort, 30);
if (!$ftp) {
throw new \Exception('خطا در اتصال به سرور FTP');
}
// ورود به FTP
if (!ftp_login($ftp, $ftpUsername, $ftpPassword)) {
ftp_close($ftp);
throw new \Exception('خطا در ورود به سرور FTP');
}
// فعال کردن حالت غیرفعال
ftp_pasv($ftp, true);
// دریافت مسیر home کاربر
$homeDir = ftp_pwd($ftp);
if ($homeDir === false) {
ftp_close($ftp);
throw new \Exception('خطا در دریافت مسیر home کاربر FTP');
}
// تنظیم مسیر نهایی نسبت به home
$remotePath = rtrim($homeDir, '/') . '/' . ltrim($ftpPath, '/') . '/' . $filename;
$remoteDir = dirname($remotePath);
// بررسی دسترسی نوشتن در مسیر
$testFile = 'test_' . time() . '.txt';
if (!@ftp_put($ftp, $testFile, 'test', FTP_ASCII)) {
ftp_close($ftp);
throw new \Exception('کاربر FTP دسترسی نوشتن ندارد');
}
ftp_delete($ftp, $testFile);
// ایجاد مسیر در صورت عدم وجود
$this->createFtpDirectory($ftp, $remoteDir);
// تغییر به مسیر مورد نظر
if (!@ftp_chdir($ftp, $remoteDir)) {
ftp_close($ftp);
throw new \Exception("خطا در تغییر به مسیر {$remoteDir} در سرور FTP");
}
// آپلود فایل
if (!ftp_put($ftp, basename($remotePath), $filepath, FTP_BINARY)) {
$error = error_get_last();
ftp_close($ftp);
throw new \Exception('خطا در آپلود فایل به سرور FTP: ' . ($error['message'] ?? 'خطای نامشخص'));
}
ftp_close($ftp);
// ذخیره اطلاعات آخرین پشتیبان FTP
$this->updateLastBackupInfo('ftp', $filename);
return $this->json([
'result' => 1,
'filename' => $filename,
'message' => 'پشتیبان با موفقیت ایجاد و به سرور FTP ارسال شد'
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => 'خطا در ایجاد و ارسال پشتیبان: ' . $e->getMessage()
], 500);
}
}
private function ensureBackupDirectoriesExist(): void
{
$directories = [
dirname($this->backupPath),
$this->backupPath
];
foreach ($directories as $dir) {
if (!file_exists($dir)) {
if (!mkdir($dir, 0755, true)) {
throw new \Exception("خطا در ایجاد پوشه {$dir}");
}
}
}
}
private function getLastBackupInfo(string $type): ?string
{
$key = $type === 'ftp' ? 'last_ftp_backup' : 'last_backup';
return $this->registryMGR->get('system_settings', $key);
}
private function updateLastBackupInfo(string $type, string $filename): void
{
$key = $type === 'ftp' ? 'last_ftp_backup' : 'last_backup';
$this->registryMGR->update('system_settings', $key, $filename);
}
private function createFtpDirectory($ftp, $dir): void
{
// اگر مسیر ریشه است، نیازی به ایجاد نیست
if ($dir === '/' || $dir === '.') {
return;
}
// بررسی وجود مسیر
if (@ftp_chdir($ftp, $dir)) {
ftp_chdir($ftp, '/');
return;
}
// ایجاد مسیر والد
$parent = dirname($dir);
$this->createFtpDirectory($ftp, $parent);
// ایجاد مسیر فعلی
$folder = basename($dir);
if (!@ftp_mkdir($ftp, $folder)) {
throw new \Exception("خطا در ایجاد پوشه {$folder} در سرور FTP. لطفاً دسترسی‌های کاربر FTP را بررسی کنید.");
}
}
}

View file

@ -57,6 +57,13 @@ final class RegistrySettingsController extends AbstractController
'appUrl' => $registryMGR->get('system', 'appUrl'),
'appSlogan' => $registryMGR->get('system', 'appSlogan'),
'verifyMobileViaSms' => filter_var($registryMGR->get('system', 'verifyMobileViaSms'), FILTER_VALIDATE_BOOLEAN),
// تنظیمات FTP
'ftpEnabled' => filter_var($registryMGR->get($rootSystem, 'ftp_enabled'), FILTER_VALIDATE_BOOLEAN),
'ftpHost' => $registryMGR->get($rootSystem, 'ftp_host') ?: '',
'ftpPort' => $registryMGR->get($rootSystem, 'ftp_port') ?: '21',
'ftpUsername' => $registryMGR->get($rootSystem, 'ftp_username') ?: '',
'ftpPassword' => $registryMGR->get($rootSystem, 'ftp_password') ?: '',
'ftpPath' => $registryMGR->get($rootSystem, 'ftp_path') ?: '',
];
return new JsonResponse([
@ -89,10 +96,86 @@ final class RegistrySettingsController extends AbstractController
$registryMGR->update('system', 'appUrl', $data['appUrl'] ?? '');
$registryMGR->update('system', 'appSlogan', $data['appSlogan'] ?? '');
$registryMGR->update('system', 'verifyMobileViaSms', $data['verifyMobileViaSms'] ? '1' : '0');
// ذخیره تنظیمات FTP
$registryMGR->update($rootSystem, 'ftp_enabled', $data['ftpEnabled'] ? '1' : '0');
$registryMGR->update($rootSystem, 'ftp_host', $data['ftpHost'] ?? '');
$registryMGR->update($rootSystem, 'ftp_port', $data['ftpPort'] ?? '21');
$registryMGR->update($rootSystem, 'ftp_username', $data['ftpUsername'] ?? '');
$registryMGR->update($rootSystem, 'ftp_password', $data['ftpPassword'] ?? '');
$registryMGR->update($rootSystem, 'ftp_path', $data['ftpPath'] ?? '');
return new JsonResponse([
'result' => 1,
'message' => 'Settings saved successfully'
]);
}
#[Route('/api/admin/registry/settings/test-ftp', name: 'app_registry_settings_test_ftp', methods: ['POST'])]
public function testFtpConnection(Request $request): JsonResponse
{
try {
$data = json_decode($request->getContent(), true);
// اعتبارسنجی داده‌های ورودی
$requiredFields = ['host', 'port', 'username', 'password', 'path'];
foreach ($requiredFields as $field) {
if (empty($data[$field])) {
return $this->json([
'success' => false,
'message' => "فیلد {$field} الزامی است"
], 400);
}
}
// اعتبارسنجی پورت
$port = (int) $data['port'];
if ($port < 1 || $port > 65535) {
return $this->json([
'success' => false,
'message' => 'پورت باید عددی بین 1 تا 65535 باشد'
], 400);
}
// ایجاد اتصال FTP
$ftp = ftp_connect($data['host'], $port, 30);
if (!$ftp) {
return $this->json([
'success' => false,
'message' => 'خطا در اتصال به سرور FTP'
], 400);
}
// تلاش برای ورود
if (!ftp_login($ftp, $data['username'], $data['password'])) {
ftp_close($ftp);
return $this->json([
'success' => false,
'message' => 'نام کاربری یا رمز عبور اشتباه است'
], 400);
}
// تست دسترسی به مسیر
if (!ftp_chdir($ftp, $data['path'])) {
ftp_close($ftp);
return $this->json([
'success' => false,
'message' => 'مسیر مورد نظر قابل دسترسی نیست'
], 400);
}
// بستن اتصال
ftp_close($ftp);
return $this->json([
'success' => true,
'message' => 'اتصال به سرور FTP با موفقیت برقرار شد'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در تست اتصال: ' . $e->getMessage()
], 500);
}
}
}

View file

@ -53,6 +53,9 @@ class HesabdariDoc
#[ORM\Column(type: Types::DECIMAL, precision: 30, scale: 0, nullable: true)]
private ?string $amount = '0';
#[ORM\Column(type: Types::FLOAT, nullable: true)]
private ?float $taxPercent = 0;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
@ -119,6 +122,12 @@ class HesabdariDoc
#[ORM\JoinTable(name: 'pairDoc')]
private Collection $pairDoc;
#[ORM\Column(length: 255, nullable: true)]
private ?string $discountType = null;
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]
private ?float $discountPercent = null;
public function __construct()
{
$this->hesabdariRows = new ArrayCollection();
@ -273,6 +282,17 @@ class HesabdariDoc
return $this;
}
public function getTaxPercent(): ?float
{
return $this->taxPercent;
}
public function setTaxPercent(?float $taxPercent): self
{
$this->taxPercent = $taxPercent;
return $this;
}
public function getMoney(): ?Money
{
return $this->money;
@ -572,4 +592,26 @@ class HesabdariDoc
return $this;
}
public function getDiscountType(): ?string
{
return $this->discountType;
}
public function setDiscountType(?string $discountType): static
{
$this->discountType = $discountType;
return $this;
}
public function getDiscountPercent(): ?float
{
return $this->discountPercent;
}
public function setDiscountPercent(?float $discountPercent): static
{
$this->discountPercent = $discountPercent;
return $this;
}
}

View file

@ -91,6 +91,12 @@ class HesabdariRow
#[ORM\Column(length: 255, nullable: true)]
private ?string $tax = null;
#[ORM\Column(length: 20, nullable: true)]
private ?string $discountType = null;
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]
private ?float $discountPercent = null;
public function __construct()
{
}
@ -338,4 +344,28 @@ class HesabdariRow
return $this;
}
public function getDiscountType(): ?string
{
return $this->discountType;
}
public function setDiscountType(?string $discountType): self
{
$this->discountType = $discountType;
return $this;
}
public function getDiscountPercent(): ?float
{
return $this->discountPercent;
}
public function setDiscountPercent(?float $discountPercent): self
{
$this->discountPercent = $discountPercent;
return $this;
}
}

View file

@ -64,6 +64,8 @@ class Explore
$person = self::ExplorePerson($item->getPerson());
} elseif ($item->getRef()->getCode() == '104') {
$result['discountAll'] = $item->getBd();
$result['discountType'] = $hesabdariDoc->getDiscountType();
$result['discountPercent'] = $hesabdariDoc->getDiscountPercent();
} elseif ($item->getRef()->getCode() == '61') {
$result['transferCost'] = $item->getBs();
}
@ -72,6 +74,10 @@ class Explore
$result['discountAll'] = 0;
if (!array_key_exists('transferCost', $result))
$result['transferCost'] = 0;
if (!array_key_exists('discountType', $result))
$result['discountType'] = 'fixed';
if (!array_key_exists('discountPercent', $result))
$result['discountPercent'] = null;
$result['person'] = $person;
$result['pair_docs'] = [];
foreach ($hesabdariDoc->getPairDoc() as $pair) {
@ -105,6 +111,8 @@ class Explore
$person = self::ExplorePerson($item->getPerson());
} elseif ($item->getRef()->getCode() == '51') {
$result['discountAll'] = $item->getBs();
$result['discountType'] = $hesabdariDoc->getDiscountType();
$result['discountPercent'] = $hesabdariDoc->getDiscountPercent();
} elseif ($item->getRef()->getCode() == '90') {
$result['transferCost'] = $item->getBd();
}
@ -113,6 +121,10 @@ class Explore
$result['discountAll'] = 0;
if (!array_key_exists('transferCost', $result))
$result['transferCost'] = 0;
if (!array_key_exists('discountType', $result))
$result['discountType'] = 'fixed';
if (!array_key_exists('discountPercent', $result))
$result['discountPercent'] = null;
$result['person'] = $person;
return $result;
}
@ -126,9 +138,20 @@ class Explore
$person = self::ExplorePerson($item->getPerson());
} elseif ($item->getCommodity()) {
$commodities[] = Explore::ExploreCommodity($item->getCommodity(), $item->getCommdityCount(), $item->getDes());
} elseif ($item->getRef()->getCode() == '104') {
$result['discountAll'] = $item->getBs();
$result['discountType'] = $hesabdariDoc->getDiscountType();
$result['discountPercent'] = $hesabdariDoc->getDiscountPercent();
}
}
if (!array_key_exists('discountAll', $result))
$result['discountAll'] = 0;
if (!array_key_exists('discountType', $result))
$result['discountType'] = 'fixed';
if (!array_key_exists('discountPercent', $result))
$result['discountPercent'] = null;
$result['person'] = $person;
$result['commodities'] = $commodities;
return $result;
}
public static function ExploreHesabdariDoc(HesabdariDoc $doc)

View file

@ -35,26 +35,26 @@ print_header() {
echo -e "${BOLD}${BLUE} Hesabix Installation Script ${NC}"
echo -e "${BOLD}${BLUE}=================================================${NC}"
echo -e "${YELLOW}Hesabix is a powerful open-source accounting software${NC}"
echo -e "${YELLOW}developed with by Babak Alizadeh (alizadeh.babak)${NC}"
echo -e "${YELLOW}developed with ❤ by Babak Alizadeh (alizadeh.babak)${NC}"
echo -e "${YELLOW}License: GNU GPL v3${NC}"
echo -e "${YELLOW}Website: ${UNDERLINE}https://hesabix.ir${NC}"
echo -e "${YELLOW}Support us: ${UNDERLINE}https://hesabix.ir/page/sponsors${NC} "
echo -e "${YELLOW}Support us: ${UNDERLINE}https://hesabix.ir/page/sponsors${NC} ❤"
echo -e "${BOLD}${BLUE}=================================================${NC}\n"
# Show prerequisites
echo -e "${BOLD}${YELLOW}Prerequisites:${NC}"
echo -e "1. A domain name pointing to this server"
echo -e "2. DNS records properly configured:"
echo -e " A record pointing to server IP"
echo -e " www subdomain pointing to server IP"
echo -e " • A record pointing to server IP"
echo -e " • www subdomain pointing to server IP"
echo -e "3. Port 80 and 443 open and accessible"
echo -e "4. At least 2GB of free disk space"
echo -e "5. At least 1GB of RAM"
echo -e "\n${BOLD}${YELLOW}Important Notes:${NC}"
echo -e " SSL certificate installation requires proper DNS configuration"
echo -e " Domain must be accessible from the internet"
echo -e " Installation may take 10-15 minutes"
echo -e " System will be automatically rolled back if installation fails"
echo -e "• SSL certificate installation requires proper DNS configuration"
echo -e "• Domain must be accessible from the internet"
echo -e "• Installation may take 10-15 minutes"
echo -e "• System will be automatically rolled back if installation fails"
echo -e "\n${BOLD}${YELLOW}Do you want to continue?${NC}"
read -p "Press Enter to continue or Ctrl+C to abort..."
echo -e "${BOLD}${BLUE}=================================================${NC}\n"
@ -487,7 +487,7 @@ setup_ssl() {
echo -e "2. Domain is pointing to this server's IP address"
echo -e "3. Port 80 is accessible from the internet"
echo -e "\n${YELLOW}You can run SSL setup later using:${NC}"
echo -e "${GREEN}sudo certbot --apache -d $domain -d www.$domain${NC}"
echo -e "${GREEN}sudo certbot --apache -d $domain${NC}"
return 1
fi
@ -504,7 +504,7 @@ setup_ssl() {
if ! systemctl is-active --quiet apache2; then
log_message "ERROR" "Apache is not running. Please start Apache first."
return 1
}
fi
# Try to setup SSL with multiple attempts
while [[ $attempt -le $max_attempts ]] && [[ $success == false ]]; do
@ -515,8 +515,8 @@ setup_ssl() {
log_message "WARNING" "Failed to stop Apache, continuing anyway..."
}
# Run certbot
if certbot --apache -d "$domain" -d "www.$domain" --non-interactive --agree-tos --email "admin@$domain" --force-renewal; then
# Run certbot only for main domain
if certbot --apache -d "$domain" --non-interactive --agree-tos --email "admin@$domain" --force-renewal; then
success=true
log_message "INFO" "SSL setup completed successfully"
else
@ -550,10 +550,10 @@ setup_ssl() {
echo -e "2. Port 80 is blocked by firewall"
echo -e "3. Let's Encrypt rate limit exceeded"
echo -e "\n${YELLOW}You can try setting up SSL manually using:${NC}"
echo -e "${GREEN}sudo certbot --apache -d $domain -d www.$domain${NC}"
echo -e "${GREEN}sudo certbot --apache -d $domain${NC}"
echo -e "\n${YELLOW}Or check the logs:${NC}"
echo -e "${GREEN}sudo certbot certificates${NC}"
echo -e "${GREEN}sudo certbot --apache -d $domain -d www.$domain --dry-run${NC}"
echo -e "${GREEN}sudo certbot --apache -d $domain --dry-run${NC}"
return 1
fi
}
@ -656,7 +656,8 @@ setup_database() {
local db_name="$base_db_name"
local db_user="hesabix_user"
local db_password
db_password=$(openssl rand -base64 12)
# Generate password with only alphanumeric characters
db_password=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 12 | head -n 1)
local domain_path="/var/www/html/$domain"
local counter=1
@ -868,30 +869,32 @@ setup_web_ui() {
cd "$webui_path" || handle_error "Failed to change to webUI directory"
# Set initial permissions for npm operations
chown -R "$SUDO_USER:$SUDO_USER" "$webui_path"
chmod -R 777 "$webui_path"
# Install dependencies
log_message "INFO" "Installing web UI dependencies..."
timeout "$NPM_TIMEOUT" npm install || handle_error "Failed to install web UI dependencies"
# Set proper permissions for webUI directory
log_message "INFO" "Setting proper permissions for webUI directory..."
# Build web UI
log_message "INFO" "Building web UI..."
timeout "$NPM_TIMEOUT" npm run build-only || handle_error "Failed to build web UI"
# After build, set final ownership to apache
chown -R "$apache_user:$apache_user" "$webui_path"
chmod -R 755 "$webui_path"
# Set execute permissions for node_modules/.bin
if [[ -d "$webui_path/node_modules/.bin" ]]; then
chmod -R +x "$webui_path/node_modules/.bin"
fi
# Set final permissions
find "$webui_path" -type d -exec chmod 755 {} \;
find "$webui_path" -type f -exec chmod 644 {} \;
# Set proper permissions for node_modules
log_message "INFO" "Setting proper permissions for node_modules directory..."
# Set special permissions for node_modules
if [[ -d "$webui_path/node_modules" ]]; then
chmod -R 755 "$webui_path/node_modules"
find "$webui_path/node_modules" -type f -exec chmod 644 {} \;
find "$webui_path/node_modules" -type d -exec chmod 755 {} \;
find "$webui_path/node_modules/.bin" -type f -exec chmod 755 {} \;
# Build web UI
log_message "INFO" "Building web UI..."
timeout "$NPM_TIMEOUT" npm run build-only || handle_error "Failed to build web UI"
fi
log_message "INFO" "Web UI setup completed"
}
@ -950,14 +953,14 @@ confirm_installation() {
echo -e "${BOLD}${BLUE} Installation Confirmation ${NC}"
echo -e "${BOLD}${BLUE}=================================================${NC}"
echo -e "${YELLOW}This script will install:${NC}"
echo -e " PHP and required extensions"
echo -e " MySQL/MariaDB"
echo -e " Apache"
echo -e " Node.js"
echo -e " Composer"
echo -e " phpMyAdmin"
echo -e " Hesabix Core"
echo -e " Hesabix Web UI"
echo -e "• PHP and required extensions"
echo -e "• MySQL/MariaDB"
echo -e "• Apache"
echo -e "• Node.js"
echo -e "• Composer"
echo -e "• phpMyAdmin"
echo -e "• Hesabix Core"
echo -e "• Hesabix Web UI"
echo -e "\n${YELLOW}The installation will require approximately 2GB of disk space.${NC}"
echo -e "${BOLD}${BLUE}=================================================${NC}\n"
@ -1012,7 +1015,7 @@ show_installation_summary() {
# Get database password from env file
if [[ -f "$env_file" ]]; then
db_password=$(php -r "include '$env_file'; echo \$env['DATABASE_URL']; echo PHP_EOL;" | grep -oP '(?<=://[^:]+:)[^@]+(?=@)')
db_password=$(php -r "include '$env_file'; echo \$env['DATABASE_URL']; echo PHP_EOL;" | sed -n 's/.*:\/\/[^:]*:\([^@]*\)@.*/\1/p')
fi
log_message "INFO" "Showing installation summary..."
@ -1047,7 +1050,7 @@ show_installation_summary() {
echo -e "\n${YELLOW}To get database information, you can:${NC}"
echo -e "1. Check the file: $env_file"
echo -e "2. Run this command to extract password:"
echo -e " ${GREEN}php -r \"include '$env_file'; echo \$env['DATABASE_URL']; echo PHP_EOL;\" | grep -oP '(?<=://[^:]+:)[^@]+(?=@)'${NC}"
echo -e " ${GREEN}php -r \"include '$env_file'; echo \$env['DATABASE_URL']; echo PHP_EOL;\" | sed -n 's/.*:\/\/[^:]*:\\([^@]*\\)@.*/\\1/p'${NC}"
echo -e "3. Or check MySQL directly:"
echo -e " ${GREEN}mysql -u root -e \"SELECT User, Host FROM mysql.user WHERE User='$db_user';\"${NC}"
fi
@ -1068,10 +1071,10 @@ show_installation_summary() {
echo -e "4. Register the first user (system administrator)"
echo -e "\n${YELLOW}Support:${NC}"
echo -e " Developer: Babak Alizadeh (alizadeh.babak)"
echo -e " License: GNU GPL v3"
echo -e " Website: ${UNDERLINE}https://hesabix.ir${NC}"
echo -e " Support us: ${UNDERLINE}https://hesabix.ir/page/sponsors${NC} "
echo -e "• Developer: Babak Alizadeh (alizadeh.babak)"
echo -e "• License: GNU GPL v3"
echo -e "• Website: ${UNDERLINE}https://hesabix.ir${NC}"
echo -e "• Support us: ${UNDERLINE}https://hesabix.ir/page/sponsors${NC} ❤"
echo -e "\n${GREEN}Installation completed successfully!${NC}"
echo -e "${BOLD}${BLUE}=================================================${NC}"
@ -1087,9 +1090,9 @@ display_telemetry_consent() {
echo -e "${RED} Anonymous Data Collection "
echo -e "${RED}================================================="
echo -e "${BLUE}To improve Hesabix, we would like to collect anonymous data:"
echo -e "${BLUE} System information (OS, PHP, MySQL versions)"
echo -e "${BLUE} Installation path and domain"
echo -e "${BLUE} Installation date"
echo -e "${BLUE}• System information (OS, PHP, MySQL versions)"
echo -e "${BLUE}• Installation path and domain"
echo -e "${BLUE}• Installation date"
read -p "Do you agree? (y/n) [n]: " response
[[ "$response" =~ ^[Yy]$ ]] && SEND_TELEMETRY=true

0
webUI/.gitignore vendored Executable file → Normal file
View file

0
webUI/LICENSE Executable file → Normal file
View file

0
webUI/env.d.ts vendored Executable file → Normal file
View file

0
webUI/index.html Executable file → Normal file
View file

0
webUI/package.json Executable file → Normal file
View file

0
webUI/public/.htaccess Executable file → Normal file
View file

0
webUI/public/dashmix/dashmix.app.min.js vendored Executable file → Normal file
View file

0
webUI/public/dashmix/dashmix.min.css vendored Executable file → Normal file
View file

0
webUI/public/favicon.ico Executable file → Normal file
View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

0
webUI/public/fonts/fontawesome/fa-brands-400.ttf Executable file → Normal file
View file

0
webUI/public/fonts/fontawesome/fa-brands-400.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/fontawesome/fa-regular-400.ttf Executable file → Normal file
View file

0
webUI/public/fonts/fontawesome/fa-regular-400.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/fontawesome/fa-solid-900.ttf Executable file → Normal file
View file

0
webUI/public/fonts/fontawesome/fa-solid-900.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/fontawesome/fa-v4compatibility.ttf Executable file → Normal file
View file

View file

0
webUI/public/fonts/inter/inter-v11-latin-300.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/inter/inter-v11-latin-500.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/inter/inter-v11-latin-600.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/inter/inter-v11-latin-700.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/inter/inter-v11-latin-800.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/inter/inter-v11-latin-900.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/inter/inter-v11-latin-regular.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-Black-FD.eot Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-Black-FD.ttf Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-Black-FD.woff Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-Black-FD.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-Bold-FD.eot Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-Bold-FD.ttf Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-Bold-FD.woff Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-Bold-FD.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-FD.eot Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-FD.ttf Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-FD.woff Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-FD.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-Light-FD.eot Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-Light-FD.ttf Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-Light-FD.woff Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-Light-FD.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-SemiBold-FD.eot Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-SemiBold-FD.ttf Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-SemiBold-FD.woff Executable file → Normal file
View file

0
webUI/public/fonts/sahel/Sahel-SemiBold-FD.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/sahel/sahel.css Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Bold-FD.eot Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Bold-FD.ttf Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Bold-FD.woff Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Bold-FD.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-FD.eot Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-FD.ttf Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-FD.woff Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-FD.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Light-FD.eot Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Light-FD.ttf Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Light-FD.woff Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Light-FD.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Medium-FD.eot Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Medium-FD.ttf Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Medium-FD.woff Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Medium-FD.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Thin-FD.eot Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Thin-FD.ttf Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Thin-FD.woff Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/Shabnam-Thin-FD.woff2 Executable file → Normal file
View file

0
webUI/public/fonts/shabnam/shabnam.css Executable file → Normal file
View file

View file

View file

Before

Width:  |  Height:  |  Size: 235 KiB

After

Width:  |  Height:  |  Size: 235 KiB

View file

View file

View file

0
webUI/public/fonts/vazir/ttf/Vazirmatn-Black.ttf Executable file → Normal file
View file

0
webUI/public/fonts/vazir/ttf/Vazirmatn-Bold.ttf Executable file → Normal file
View file

0
webUI/public/fonts/vazir/ttf/Vazirmatn-ExtraBold.ttf Executable file → Normal file
View file

0
webUI/public/fonts/vazir/ttf/Vazirmatn-ExtraLight.ttf Executable file → Normal file
View file

0
webUI/public/fonts/vazir/ttf/Vazirmatn-Light.ttf Executable file → Normal file
View file

0
webUI/public/fonts/vazir/ttf/Vazirmatn-Medium.ttf Executable file → Normal file
View file

0
webUI/public/fonts/vazir/ttf/Vazirmatn-Regular.ttf Executable file → Normal file
View file

0
webUI/public/fonts/vazir/ttf/Vazirmatn-SemiBold.ttf Executable file → Normal file
View file

0
webUI/public/fonts/vazir/ttf/Vazirmatn-Thin.ttf Executable file → Normal file
View file

0
webUI/public/fonts/vazir/variable/Vazirmatn[wght].ttf Executable file → Normal file
View file

0
webUI/public/fonts/vazir/vazir.css Executable file → Normal file
View file

View file

0
webUI/public/fonts/vazir/webfonts/Vazirmatn-Bold.woff2 Executable file → Normal file
View file

View file

View file

View file

Some files were not shown because too many files have changed in this diff Show more