Compare commits

...

10 commits

20 changed files with 1282 additions and 211 deletions

View file

@ -23,6 +23,20 @@ services:
networks:
- hesabix-network
restart: unless-stopped
command: >
bash -c "
apt-get update &&
apt-get install -y curl unzip php-mbstring php-gd php-soap &&
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer &&
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - &&
apt-get install -y nodejs &&
cd /var/www/html/hesabixCore &&
composer install &&
cd /var/www/html/webUI &&
npm install &&
npm run build-only &&
apache2-foreground
"
# Database service
db:
@ -34,7 +48,7 @@ services:
- MYSQL_PASSWORD=hesabix_password
volumes:
- mysql_data:/var/lib/mysql
- ./hesabixBackup:/docker-entrypoint-initdb.d
- ./hesabixBackup/databasefiles:/docker-entrypoint-initdb.d
networks:
- hesabix-network
healthcheck:
@ -43,6 +57,12 @@ services:
timeout: 5s
retries: 5
restart: unless-stopped
command: >
bash -c "
docker-entrypoint.sh mysqld &
sleep 30 &&
mysql -u root -proot_password hesabix_db < /docker-entrypoint-initdb.d/hesabix-db-default.sql
"
# phpMyAdmin service
phpmyadmin:

View file

@ -1938,4 +1938,5 @@ class PersonsController extends AbstractController
return new JsonResponse(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}

View file

@ -7,16 +7,11 @@ 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\PlugHrmDoc;
use App\Entity\PlugHrmDocItem;
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;
class DocsController extends AbstractController
{
@ -28,24 +23,209 @@ class DocsController extends AbstractController
}
#[Route('/api/hrm/docs/list', name: 'hrm_docs_list', methods: ['POST'])]
public function list(Request $request): JsonResponse
public function list(Request $request, Access $access): JsonResponse
{
// TODO: پیاده‌سازی دریافت لیست اسناد حقوق
return new JsonResponse([]);
try {
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$acc = $access->hasRole('hrm');
// دریافت پارامترهای فیلتر
$page = $params['page'] ?? 1;
$limit = $params['limit'] ?? 20;
$search = $params['search'] ?? '';
$fromDate = $params['fromDate'] ?? null;
$toDate = $params['toDate'] ?? null;
$personId = $params['personId'] ?? null;
// ایجاد کوئری
$qb = $this->entityManager->createQueryBuilder();
$qb->select('d')
->from(PlugHrmDoc::class, 'd')
->where('d.business = :bid')
->setParameter('bid', $acc['bid']);
// اعمال فیلترها
if ($search) {
$qb->andWhere('d.description LIKE :search')
->setParameter('search', '%' . $search . '%');
}
if ($fromDate) {
$qb->andWhere('d.date >= :fromDate')
->setParameter('fromDate', $fromDate);
}
if ($toDate) {
$qb->andWhere('d.date <= :toDate')
->setParameter('toDate', $toDate);
}
if ($personId) {
$qb->andWhere('d.person = :personId')
->setParameter('personId', $personId);
}
// محاسبه تعداد کل رکوردها
$countQb = clone $qb;
$countQb->select('COUNT(d.id)');
$total = $countQb->getQuery()->getSingleScalarResult();
// اعمال مرتب‌سازی و صفحه‌بندی
$qb->orderBy('d.date', 'DESC')
->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit);
$docs = $qb->getQuery()->getResult();
// تبدیل نتایج به آرایه
$result = [];
foreach ($docs as $doc) {
$result[] = [
'id' => $doc->getId(),
'date' => $doc->getDate(),
'description' => $doc->getDescription(),
'creator' => $doc->getCreator() ? [
'id' => $doc->getCreator()->getId(),
'name' => $doc->getCreator()->getFullName()
] : null,
'total' => $this->calculateTotalAmount($doc),
'accounting_doc' => $doc->getHesabdariDoc() ? 'صدور شده' : 'صدور نشده',
'status' => $doc->getHesabdariDoc() ? 'تایید شده' : 'تایید نشده'
];
}
return new JsonResponse([
'success' => true,
'data' => $result,
'total' => $total,
'page' => $page,
'limit' => $limit
]);
} catch (\Exception $e) {
return new JsonResponse(['error' => 'خطا در دریافت لیست اسناد: ' . $e->getMessage()], 500);
}
}
private function calculateTotalAmount(PlugHrmDoc $doc): int
{
$total = 0;
foreach ($doc->getItems() as $item) {
$total += $item->getBaseSalary();
$total += $item->getNight();
$total += $item->getShift();
$total += $item->getOvertime();
}
return $total;
}
#[Route('/api/hrm/docs/get/{id}', name: 'hrm_docs_get', methods: ['POST'])]
public function get(int $id): JsonResponse
public function get(int $id, Access $access): JsonResponse
{
// TODO: پیاده‌سازی دریافت اطلاعات یک سند حقوق
return new JsonResponse([]);
try {
$acc = $access->hasRole('hrm');
$doc = $this->entityManager->getRepository(PlugHrmDoc::class)->findOneBy([
'id' => $id,
'business' => $acc['bid']
]);
if (!$doc) {
return new JsonResponse(['error' => 'سند مورد نظر یافت نشد'], 404);
}
$items = [];
foreach ($doc->getItems() as $item) {
$items[] = [
'id' => $item->getId(),
'person' => [
'id' => $item->getPerson()->getId(),
'name' => $item->getPerson()->getNikename(),
'code' => $item->getPerson()->getCode(),
],
'baseSalary' => $item->getBaseSalary(),
'overtime' => $item->getOvertime(),
'shift' => $item->getShift(),
'night' => $item->getNight(),
'description' => $item->getDescription()
];
}
return new JsonResponse([
'success' => true,
'data' => [
'id' => $doc->getId(),
'date' => $doc->getDate(),
'description' => $doc->getDescription(),
'items' => $items
]
]);
} catch (\Exception $e) {
return new JsonResponse(['error' => 'خطا در دریافت اطلاعات سند: ' . $e->getMessage()], 500);
}
}
#[Route('/api/hrm/docs/insert', name: 'hrm_docs_insert', methods: ['POST'])]
public function insert(Request $request): JsonResponse
public function insert(Request $request, Access $access, Log $log): JsonResponse
{
// TODO: پیاده‌سازی ثبت سند حقوق جدید
return new JsonResponse([]);
try {
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$acc = $access->hasRole('hrm');
// بررسی داده‌های ورودی
if (empty($params['date']) || empty($params['description']) || empty($params['items'])) {
return new JsonResponse(['error' => 'اطلاعات ناقص است'], 400);
}
// ایجاد سند جدید
$doc = new PlugHrmDoc();
$doc->setDate($params['date']);
$doc->setCreator($this->getUser());
$doc->setBusiness($acc['bid']);
$doc->setDescription($params['description']);
$doc->setCreateDate(time());
// افزودن آیتم‌ها
foreach ($params['items'] as $itemData) {
if (empty($itemData['person'])) {
continue;
}
$item = new PlugHrmDocItem();
$item->setPerson($this->entityManager->getReference(Person::class, $itemData['person']));
$item->setBaseSalary($itemData['baseSalary'] ?? 0);
$item->setOvertime($itemData['overtime'] ?? 0);
$item->setShift($itemData['shift'] ?? 0);
$item->setNight($itemData['night'] ?? 0);
$item->setDescription($itemData['description'] ?? '');
$item->setDoc($doc);
$this->entityManager->persist($item);
}
$this->entityManager->persist($doc);
$this->entityManager->flush();
return new JsonResponse([
'success' => true,
'message' => 'سند با موفقیت ثبت شد',
'data' => [
'id' => $doc->getId()
]
]);
} catch (\Exception $e) {
return new JsonResponse(['error' => 'خطا در ثبت سند: ' . $e->getMessage()], 500);
}
}
#[Route('/api/hrm/docs/update', name: 'hrm_docs_update', methods: ['POST'])]
@ -56,9 +236,47 @@ class DocsController extends AbstractController
}
#[Route('/api/hrm/docs/delete', name: 'hrm_docs_delete', methods: ['POST'])]
public function delete(Request $request): JsonResponse
public function delete(Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
// TODO: پیاده‌سازی حذف سند حقوق
return new JsonResponse([]);
try {
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$acc = $access->hasRole('hrm');
$id = $params['id'] ?? null;
if (!$id) {
return new JsonResponse(['error' => 'شناسه سند الزامی است'], 400);
}
$doc = $entityManager->getRepository(PlugHrmDoc::class)->findOneBy([
'id' => $id,
'business' => $acc['bid']
]);
if (!$doc) {
return new JsonResponse(['error' => 'سند مورد نظر یافت نشد'], 404);
}
// حذف آیتم‌های سند
foreach ($doc->getItems() as $item) {
$entityManager->remove($item);
}
// حذف سند حسابداری در صورت وجود
if($doc->getHesabdariDoc()){
$entityManager->remove($doc->getHesabdariDoc());
}
// حذف سند
$entityManager->remove($doc);
$entityManager->flush();
return new JsonResponse(['success' => true, 'message' => 'سند با موفقیت حذف شد']);
} catch (\Exception $e) {
return new JsonResponse(['error' => 'خطا در حذف سند: ' . $e->getMessage()], 500);
}
}
}

View file

@ -30,6 +30,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Entity\BankAccount;
use App\Entity\Cashdesk;
use App\Entity\Salary;
use App\Entity\Year;
class SellController extends AbstractController
{
@ -46,8 +47,16 @@ class SellController extends AbstractController
'code' => $code,
'money' => $acc['money']
]);
if (count($doc->getRelatedDocs()) != 0)
if (!$doc) {
$canEdit = false;
}
$year = $entityManager->getRepository(Year::class)->findOneBy([
'bid' => $acc['bid'],
'head' => true
]);
if ($doc->getYear()->getId() != $year->getId()) {
$canEdit = false;
}
$tickets = $entityManager->getRepository(StoreroomTicket::class)->findBy(['doc' => $doc]);
if (count($tickets) != 0)
@ -212,11 +221,11 @@ 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->setDiscountPercent((float) $params['discountPercent']);
}
}
$doc->setDes($params['des']);
@ -711,10 +720,10 @@ class SellController extends AbstractController
$params['printers'] = $params['printers'] ?? false;
$params['pdf'] = $params['pdf'] ?? true;
$params['posPrint'] = $params['posPrint'] ?? false;
// دریافت تنظیمات پیش‌فرض از PrintOptions
$printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid' => $acc['bid']]);
// تنظیم مقادیر پیش‌فرض از تنظیمات ذخیره شده
$defaultOptions = [
'note' => $printSettings ? $printSettings->isSellNote() : true,
@ -726,7 +735,7 @@ class SellController extends AbstractController
'invoiceIndex' => $printSettings ? $printSettings->isSellInvoiceIndex() : true,
'businessStamp' => $printSettings ? $printSettings->isSellBusinessStamp() : true
];
// اولویت با پارامترهای ارسالی است
$printOptions = array_merge($defaultOptions, $params['printOptions'] ?? []);
@ -750,6 +759,23 @@ class SellController extends AbstractController
}
}
$pdfPid = 0;
// فیلد جدید وضعیت حساب مشتری
$personItems = $entityManager->getRepository(HesabdariRow::class)->findBy(['bid' => $acc['bid'], 'person' => $person]);
$accountStatus = [];
$bs = 0;
$bd = 0;
foreach ($personItems as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
}
if ($bs > $bd) {
$accountStatus['label'] = 'بستانکار';
$accountStatus['value'] = $bs - $bd;
} else {
$accountStatus['label'] = 'بدهکار';
$accountStatus['value'] = $bd - $bs;
}
if ($params['pdf'] == true || $params['printers'] == true) {
$note = '';
if ($printSettings) {
@ -759,9 +785,10 @@ class SellController extends AbstractController
$acc['bid'],
$this->getUser(),
$this->renderView('pdf/printers/sell.html.twig', [
'accountStatus' => $accountStatus,
'bid' => $acc['bid'],
'doc' => $doc,
'rows' => array_map(function($row) {
'rows' => array_map(function ($row) {
return [
'commodity' => $row->getCommodity(),
'commodityCount' => $row->getCommdityCount(),
@ -793,7 +820,7 @@ class SellController extends AbstractController
$this->renderView('pdf/posPrinters/justSell.html.twig', [
'bid' => $acc['bid'],
'doc' => $doc,
'rows' => array_map(function($row) {
'rows' => array_map(function ($row) {
return [
'commodity' => $row->getCommodity(),
'commodityCount' => $row->getCommdityCount(),
@ -868,7 +895,8 @@ class SellController extends AbstractController
Access $access,
Log $log,
EntityManagerInterface $entityManager,
registryMGR $registryMGR
registryMGR $registryMGR,
Jdate $jdate
): JsonResponse {
$params = [];
if ($content = $request->getContent()) {
@ -932,12 +960,22 @@ class SellController extends AbstractController
}
// تنظیم اطلاعات اصلی فاکتور
$doc->setDes($params['invoiceDescription']);
$doc->setDate($params['invoiceDate']);
$doc->setTaxPercent($params['taxPercent'] ?? 0);
if (isset($params['invoiceDescription'])) {
$doc->setDes($params['invoiceDescription']);
} else {
$doc->setDes('');
}
if (isset($params['invoiceDate'])) {
$doc->setDate($params['invoiceDate']);
} else {
$doc->setDate($jdate->jdate('Y/n/d', time()));
}
if (isset($params['taxPercent'])) {
$doc->setTaxPercent($params['taxPercent'] ?? 0);
}
// افزودن هزینه حمل
if ($params['shippingCost'] > 0) {
if (isset($params['shippingCost']) && $params['shippingCost'] > 0) {
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDes('حمل و نقل کالا');
$hesabdariRow->setBid($acc['bid']);
@ -952,17 +990,21 @@ class SellController extends AbstractController
// افزودن تخفیف کلی
$totalDiscount = 0;
if ($params['discountType'] === 'percent') {
if (isset($params['discountType']) && $params['discountType'] === 'percent') {
$totalDiscount = round(($params['totalInvoice'] * $params['discountPercent']) / 100);
$doc->setDiscountType('percent');
$doc->setDiscountPercent((float)$params['discountPercent']);
$doc->setDiscountPercent((float) $params['discountPercent']);
} else {
$totalDiscount = $params['totalDiscount'];
if (isset($params['totalDiscount']) && $params['totalDiscount'] > 0) {
$totalDiscount = $params['totalDiscount'];
} else {
$totalDiscount = 0;
}
$doc->setDiscountType('fixed');
$doc->setDiscountPercent(null);
}
if ($totalDiscount > 0) {
if (isset($totalDiscount) && $totalDiscount > 0) {
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDes('تخفیف فاکتور');
$hesabdariRow->setBid($acc['bid']);
@ -1046,10 +1088,25 @@ class SellController extends AbstractController
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => '3']);
$hesabdariRow->setRef($ref);
$person = $entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'id' => $params['customer']
]);
if (!isset($params['customer']) || $params['customer'] == '') {
$person = $entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'nikename' => 'مشتری پیش فرض'
]);
if (!$person) {
$person = new Person();
$person->setBid($acc['bid']);
$person->setNikename('مشتری پیش فرض');
$person->setCode($provider->getAccountingCode($acc['bid'], 'person'));
$entityManager->persist($person);
}
} else {
$person = $entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'id' => $params['customer']
]);
}
if (!$person) {
throw new \Exception('خریدار یافت نشد');
}
@ -1082,10 +1139,10 @@ class SellController extends AbstractController
$paymentDoc->setDate($params['invoiceDate']);
$paymentDoc->setDes($payment['description'] ?? 'دریافت وجه فاکتور فروش شماره ' . $doc->getCode());
$paymentDoc->setAmount($payment['amount']);
// ایجاد ارتباط با فاکتور اصلی
$doc->addRelatedDoc($paymentDoc);
// ایجاد سطرهای حسابداری بر اساس نوع پرداخت
if ($payment['type'] === 'bank') {
// دریافت از طریق حساب بانکی
@ -1191,7 +1248,8 @@ class SellController extends AbstractController
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => $e->getMessage()
'message' => $e->getMessage(),
'data' => $params
]);
}
}
@ -1232,7 +1290,7 @@ class SellController extends AbstractController
// دریافت اسناد پرداخت مرتبط
$relatedDocs = $doc->getRelatedDocs();
foreach ($relatedDocs as $relatedDoc) {
if ($relatedDoc->getType() === 'sell_receive') {
$payment = [
@ -1275,7 +1333,7 @@ class SellController extends AbstractController
$itemDiscountType = $row->getDiscountType() ?? 'fixed';
$itemDiscountPercent = $row->getDiscountPercent() ?? 0;
$itemTax = $row->getTax() ?? 0;
// محاسبه قیمت واحد و تخفیف
if ($itemDiscountType === 'percent' && $itemDiscountPercent > 0) {
// محاسبه قیمت اصلی در حالت تخفیف درصدی
@ -1285,14 +1343,14 @@ class SellController extends AbstractController
// محاسبه قیمت اصلی در حالت تخفیف مقداری
$originalPrice = $basePrice + $itemDiscount;
}
// محاسبه قیمت واحد
$unitPrice = $row->getCommdityCount() > 0 ? $originalPrice / $row->getCommdityCount() : 0;
// محاسبه قیمت خالص (بدون مالیات)
$netPrice = $basePrice;
$totalInvoice += $netPrice;
$items[] = [
'name' => [
'id' => $row->getCommodity()->getId(),

View file

@ -19,7 +19,7 @@ final class DatabaseController extends AbstractController
{
$this->registryMGR = $registryMGR;
$this->entityManager = $entityManager;
$this->backupPath = dirname(__DIR__, 2) . '/hesabixBackup/versions';
$this->backupPath = dirname(__DIR__, 4) . '/hesabixBackup/versions';
}
#[Route('/api/admin/database/backup/info', name: 'app_admin_database_backup_info', methods: ['GET'])]
@ -53,6 +53,11 @@ final class DatabaseController extends AbstractController
$filename = 'Hesabix-' . time() . '.sql';
$filepath = $this->backupPath . '/' . $filename;
// بررسی دسترسی پوشه مقصد
if (!is_writable($this->backupPath)) {
throw new \Exception('پوشه مقصد قابل نوشتن نیست: ' . $this->backupPath);
}
// دریافت تنظیمات دیتابیس از EntityManager
$connection = $this->entityManager->getConnection();
$params = $connection->getParams();
@ -65,7 +70,7 @@ final class DatabaseController extends AbstractController
// دستور mysqldump
$command = sprintf(
'mysqldump -h %s -P %s -u %s -p%s %s > %s',
'mysqldump -h %s -P %s -u %s -p%s %s > %s 2>&1',
escapeshellarg($dbHost),
escapeshellarg($dbPort),
escapeshellarg($dbUser),
@ -90,8 +95,8 @@ final class DatabaseController extends AbstractController
'message' => 'پشتیبان با موفقیت ایجاد شد'
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
return $this->json([
'result' => 0,
'message' => 'خطا در ایجاد پشتیبان: ' . $e->getMessage()
], 500);
}
@ -160,12 +165,16 @@ final class DatabaseController extends AbstractController
$remoteDir = dirname($remotePath);
// بررسی دسترسی نوشتن در مسیر
$testFile = 'test_' . time() . '.txt';
if (!@ftp_put($ftp, $testFile, 'test', FTP_ASCII)) {
$testFileLocal = tempnam(sys_get_temp_dir(), 'ftp_test_');
file_put_contents($testFileLocal, 'test');
$testFileRemote = 'test_' . time() . '.txt';
if (!@ftp_put($ftp, $testFileRemote, $testFileLocal, FTP_ASCII)) {
unlink($testFileLocal);
ftp_close($ftp);
throw new \Exception('کاربر FTP دسترسی نوشتن ندارد');
}
ftp_delete($ftp, $testFile);
ftp_delete($ftp, $testFileRemote);
unlink($testFileLocal);
// ایجاد مسیر در صورت عدم وجود
$this->createFtpDirectory($ftp, $remoteDir);
@ -177,7 +186,8 @@ final class DatabaseController extends AbstractController
}
// آپلود فایل
if (!ftp_put($ftp, basename($remotePath), $filepath, FTP_BINARY)) {
$result = ftp_put($ftp, basename($remotePath), $filepath, FTP_BINARY);
if (!$result) {
$error = error_get_last();
ftp_close($ftp);
throw new \Exception('خطا در آپلود فایل به سرور FTP: ' . ($error['message'] ?? 'خطای نامشخص'));

View file

@ -155,11 +155,13 @@ final class RegistrySettingsController extends AbstractController
}
// تست دسترسی به مسیر
if (!ftp_chdir($ftp, $data['path'])) {
if (!@ftp_chdir($ftp, $data['path'])) {
$currentDir = ftp_pwd($ftp); // دریافت مسیر فعلی
ftp_close($ftp);
return $this->json([
'success' => false,
'message' => 'مسیر مورد نظر قابل دسترسی نیست'
'message' => 'مسیر مورد نظر قابل دسترسی نیست',
'suggested_path' => $currentDir // پیشنهاد مسیر فعلی
], 400);
}

View file

@ -297,6 +297,9 @@ class Business
#[ORM\OneToMany(targetEntity: PlugGhestaDoc::class, mappedBy: 'bid', orphanRemoval: true)]
private Collection $PlugGhestaDocs;
#[ORM\OneToMany(mappedBy: 'business', targetEntity: PlugHrmDoc::class)]
private Collection $plugHrmDocs;
public function __construct()
{
$this->logs = new ArrayCollection();
@ -339,6 +342,7 @@ class Business
$this->hesabdariTables = new ArrayCollection();
$this->accountingPackageOrders = new ArrayCollection();
$this->PlugGhestaDocs = new ArrayCollection();
$this->plugHrmDocs = new ArrayCollection();
}
public function getId(): ?int
@ -2055,4 +2059,31 @@ class Business
return $this;
}
/**
* @return Collection<int, PlugHrmDoc>
*/
public function getPlugHrmDocs(): Collection
{
return $this->plugHrmDocs;
}
public function addPlugHrmDoc(PlugHrmDoc $plugHrmDoc): static
{
if (!$this->plugHrmDocs->contains($plugHrmDoc)) {
$this->plugHrmDocs->add($plugHrmDoc);
$plugHrmDoc->setBusiness($this);
}
return $this;
}
public function removePlugHrmDoc(PlugHrmDoc $plugHrmDoc): static
{
if ($this->plugHrmDocs->removeElement($plugHrmDoc)) {
if ($plugHrmDoc->getBusiness() === $this) {
$plugHrmDoc->setBusiness(null);
}
}
return $this;
}
}

View file

@ -140,6 +140,12 @@ class HesabdariDoc
#[ORM\OneToMany(targetEntity: PlugGhestaDoc::class, mappedBy: 'mainDoc', orphanRemoval: true)]
private Collection $plugGhestaDocs;
/**
* @var Collection<int, PlugHrmDoc>
*/
#[ORM\OneToMany(targetEntity: PlugHrmDoc::class, mappedBy: 'hesabdariDoc')]
private Collection $plugHrmDocs;
public function __construct()
{
$this->hesabdariRows = new ArrayCollection();
@ -151,6 +157,7 @@ class HesabdariDoc
$this->pairDoc = new ArrayCollection();
$this->plugGhestaItems = new ArrayCollection();
$this->plugGhestaDocs = new ArrayCollection();
$this->plugHrmDocs = new ArrayCollection();
}
public function getId(): ?int
@ -689,4 +696,34 @@ class HesabdariDoc
return $this;
}
/**
* @return Collection<int, PlugHrmDoc>
*/
public function getPlugHrmDocs(): Collection
{
return $this->plugHrmDocs;
}
public function addPlugHrmDoc(PlugHrmDoc $plugHrmDoc): static
{
if (!$this->plugHrmDocs->contains($plugHrmDoc)) {
$this->plugHrmDocs->add($plugHrmDoc);
$plugHrmDoc->setHesabdariDoc($this);
}
return $this;
}
public function removePlugHrmDoc(PlugHrmDoc $plugHrmDoc): static
{
if ($this->plugHrmDocs->removeElement($plugHrmDoc)) {
// set the owning side to null (unless already changed)
if ($plugHrmDoc->getHesabdariDoc() === $this) {
$plugHrmDoc->setHesabdariDoc(null);
}
}
return $this;
}
}

View file

@ -0,0 +1,144 @@
<?php
namespace App\Entity;
use App\Repository\PlugHrmDocRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PlugHrmDocRepository::class)]
class PlugHrmDoc
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $description = null;
#[ORM\Column(type: 'string', length: 10)]
private ?string $date = null;
#[ORM\ManyToOne(inversedBy: 'plugHrmDocs')]
#[ORM\JoinColumn(nullable: false)]
private ?Business $business = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?User $creator = null;
#[ORM\Column]
private ?int $createDate = null;
#[ORM\OneToMany(mappedBy: 'doc', targetEntity: PlugHrmDocItem::class, orphanRemoval: true)]
private Collection $items;
#[ORM\ManyToOne(inversedBy: 'plugHrmDocs')]
private ?HesabdariDoc $hesabdariDoc = null;
public function __construct()
{
$this->items = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(string $description): static
{
$this->description = $description;
return $this;
}
public function getDate(): ?string
{
return $this->date;
}
public function setDate(string $date): self
{
$this->date = $date;
return $this;
}
public function getBusiness(): ?Business
{
return $this->business;
}
public function setBusiness(?Business $business): static
{
$this->business = $business;
return $this;
}
public function getCreator(): ?User
{
return $this->creator;
}
public function setCreator(?User $creator): static
{
$this->creator = $creator;
return $this;
}
public function getCreateDate(): ?int
{
return $this->createDate;
}
public function setCreateDate(int $createDate): static
{
$this->createDate = $createDate;
return $this;
}
/**
* @return Collection<int, PlugHrmDocItem>
*/
public function getItems(): Collection
{
return $this->items;
}
public function addItem(PlugHrmDocItem $item): static
{
if (!$this->items->contains($item)) {
$this->items->add($item);
$item->setDoc($this);
}
return $this;
}
public function removeItem(PlugHrmDocItem $item): static
{
if ($this->items->removeElement($item)) {
if ($item->getDoc() === $this) {
$item->setDoc(null);
}
}
return $this;
}
public function getHesabdariDoc(): ?HesabdariDoc
{
return $this->hesabdariDoc;
}
public function setHesabdariDoc(?HesabdariDoc $hesabdariDoc): static
{
$this->hesabdariDoc = $hesabdariDoc;
return $this;
}
}

View file

@ -0,0 +1,125 @@
<?php
namespace App\Entity;
use App\Repository\PlugHrmDocItemRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PlugHrmDocItemRepository::class)]
class PlugHrmDocItem
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'items')]
#[ORM\JoinColumn(nullable: false)]
private ?PlugHrmDoc $doc = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?Person $person = null;
#[ORM\Column]
private ?int $baseSalary = 0;
#[ORM\Column]
private ?int $overtime = 0;
#[ORM\Column]
private ?int $shift = 0;
#[ORM\Column]
private ?int $night = 0;
#[ORM\Column(length: 255, nullable: true)]
private ?string $description = null;
public function getId(): ?int
{
return $this->id;
}
public function getDoc(): ?PlugHrmDoc
{
return $this->doc;
}
public function setDoc(?PlugHrmDoc $doc): static
{
$this->doc = $doc;
return $this;
}
public function getPerson(): ?Person
{
return $this->person;
}
public function setPerson(?Person $person): static
{
$this->person = $person;
return $this;
}
public function getBaseSalary(): ?int
{
return $this->baseSalary;
}
public function setBaseSalary(int $baseSalary): static
{
$this->baseSalary = $baseSalary;
return $this;
}
public function getOvertime(): ?int
{
return $this->overtime;
}
public function setOvertime(int $overtime): static
{
$this->overtime = $overtime;
return $this;
}
public function getShift(): ?int
{
return $this->shift;
}
public function setShift(int $shift): static
{
$this->shift = $shift;
return $this;
}
public function getNight(): ?int
{
return $this->night;
}
public function setNight(int $night): static
{
$this->night = $night;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getTotal(): int
{
return $this->baseSalary + $this->overtime + $this->shift + $this->night;
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Repository;
use App\Entity\PlugHrmDocItem;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class PlugHrmDocItemRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, PlugHrmDocItem::class);
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Repository;
use App\Entity\PlugHrmDoc;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class PlugHrmDocRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, PlugHrmDoc::class);
}
}

View file

@ -81,7 +81,7 @@
<td class="center item">تفضیل</td>
<td class="center item">بدهکار</td>
<td class="center item">بستانکار</td>
<td class="center item">سال مالی</td>
<td class="center item">شرح سند</td>
</tr>
{% set sumBs = 0 %}
{% set sumBd = 0 %}
@ -96,7 +96,7 @@
<td class="center item">{{ item.ref.name }}</td>
<td class="center item">{{ item.bd | number_format }}</td>
<td class="center item">{{ item.bs | number_format }}</td>
<td class="center item">{{ item.year.label }}</td>
<td class="center item">{{ item.doc.des }}</td>
</tr>
{% endfor %}
</tbody>

View file

@ -199,7 +199,7 @@
<tr class="stimol">
<td class="item" style="padding:1%">
<h4>توضیحات:</h4>
{{ note|nl2br }}
{{ note|raw }}
</td>
</tr>
</tbody>

View file

@ -70,7 +70,7 @@
<p>
<b>نام:
</b>
{{ bid.legalName }}
{% if bid.legalName is not empty %}{{ bid.legalName }}{% endif %}
</p>
</td>
<td class="center">
@ -78,27 +78,27 @@
<b>
شناسه ملی:
</b>
{{ bid.shenasemeli }}
{% if bid.shenasemeli is not empty %}{{ bid.shenasemeli }}{% endif %}
</p>
</td>
<td class="center">
<p>
<b>شماره ثبت:
</b>
{{ bid.shomaresabt }}
{% if bid.shomaresabt is not empty %}{{ bid.shomaresabt }}{% endif %}
</p>
</td>
<td class="center">
<p>
<b>شماره اقتصادی:
</b>
{{ bid.codeeghtesadi }}
{% if bid.codeeghtesadi is not empty %}{{ bid.codeeghtesadi }}{% endif %}
</p>
</td>
<td class="center">
<p>
<b>تلفن / نمابر:</b>
{{ bid.tel }}
{% if bid.tel is not empty %}{{ bid.tel }}{% endif %}
</p>
</td>
</tr>
@ -106,17 +106,14 @@
<td class="" colspan="1">
<p>
<b>کد پستی:</b>
{{ bid.postalcode }}
{% if bid.postalcode is not empty %}{{ bid.postalcode }}{% endif %}
</p>
</td>
<td class="" colspan="3">
<p>
<b>آدرس:
</b>
استان
{{ bid.ostan }}، شهر
{{ bid.shahrestan }}،
{{ bid.address }}
{% if bid.ostan is not empty %}استان {{ bid.ostan }}{% endif %}{% if bid.shahrestan is not empty %}، شهر {{ bid.shahrestan }}{% endif %}{% if bid.address is not empty %}، {{ bid.address }}{% endif %}
</p>
</td>
</tr>
@ -138,34 +135,34 @@
<b>نام:
</b>
{% if person.prelabel is not null %}{{ person.prelabel.label }}{% endif %}
{{ person.nikename }}
{% if person.nikename is not empty %}{{ person.nikename }}{% endif %}
</p>
</td>
<td class="center">
<p>
<b> شناسه ملی:
</b>
{{ person.shenasemeli }}
{% if person.shenasemeli is not empty %}{{ person.shenasemeli }}{% endif %}
</p>
</td>
<td class="center">
<p>
<b>شماره ثبت:
</b>
{{ person.sabt }}
{% if person.sabt is not empty %}{{ person.sabt }}{% endif %}
</p>
</td>
<td class="center">
<p>
<b>شماره اقتصادی:
</b>
{{ person.codeeghtesadi }}
{% if person.codeeghtesadi is not empty %}{{ person.codeeghtesadi }}{% endif %}
</p>
</td>
<td class="center">
<p>
<b>تلفن / نمابر:</b>
{{ person.tel }}
{% if person.tel is not empty %}{{ person.tel }}{% endif %}
</p>
</td>
</tr>
@ -173,17 +170,14 @@
<td class="" colspan="1">
<p>
<b>کد پستی:</b>
{{ person.postalcode }}
{% if person.postalcode is not empty %}{{ person.postalcode }}{% endif %}
</p>
</td>
<td class="" colspan="3">
<p>
<b>آدرس:
</b>
استان
{{ person.ostan }}، شهر
{{ person.shahr }}،
{{ person.address }}
{% if person.ostan is not empty %}استان {{ person.ostan }}{% endif %}{% if person.shahr is not empty %}، شهر {{ person.shahr }}{% endif %}{% if person.address is not empty %}، {{ person.address }}{% endif %}
</p>
</td>
</tr>
@ -230,7 +224,12 @@
<td class="center item">
{% if item.commodityCount > 0 %}
{% if item.showPercentDiscount %}
{% set originalPrice = item.bs / (1 - (item.discountPercent / 100)) %}
{% set discountDivisor = 1 - (item.discountPercent / 100) %}
{% if discountDivisor <= 0 %}
{% set originalPrice = item.bs %}
{% else %}
{% set originalPrice = item.bs / discountDivisor %}
{% endif %}
{% set unitPrice = originalPrice / item.commodityCount %}
{% else %}
{% set originalPrice = item.bs + item.discount %}
@ -252,7 +251,12 @@
</td>
<td class="center item">
{% if item.showPercentDiscount %}
{% set originalPrice = item.bs / (1 - (item.discountPercent / 100)) %}
{% set discountDivisor = 1 - (item.discountPercent / 100) %}
{% if discountDivisor <= 0 %}
{% set originalPrice = item.bs %}
{% else %}
{% set originalPrice = item.bs / discountDivisor %}
{% endif %}
{{ originalPrice|round|number_format }} {{ doc.money.shortName }}
{% else %}
{{ (item.bs + item.discount)|number_format }} {{ doc.money.shortName }}
@ -300,6 +304,17 @@
</li>
</ul>
{% endif %}
{# فیلد جدید وضعیت حساب مشتری #}
{% if accountStatus is defined %}
<h4 class="">
وضعیت حساب مشتری با احتساب این فاکتور:
{{ accountStatus.value | number_format}}
{{ doc.money.shortName }}
{{ accountStatus.label }}
</h4>
{% endif %}
</div>
</h4>
</td>

View file

@ -1,72 +1,251 @@
<template>
<v-container>
<v-card>
<v-card-title class="d-flex align-center">
{{ $t('drawer.hrm_docs') }}
<v-spacer></v-spacer>
<v-btn color="primary" prepend-icon="mdi-plus" to="/acc/hrm/docs/mod/">
{{ $t('dialog.add_new') }}
</v-btn>
</v-card-title>
<v-card-text>
<v-data-table
:headers="headers"
:items="items"
:loading="loading"
class="elevation-1"
>
<template v-slot:item.actions="{ item }">
<v-btn
icon="mdi-eye"
variant="text"
size="small"
:to="'/acc/hrm/docs/view/' + item.id"
></v-btn>
<v-btn
icon="mdi-pencil"
variant="text"
size="small"
:to="'/acc/hrm/docs/mod/' + item.id"
></v-btn>
<div>
<v-toolbar color="toolbar" title="لیست حقوق">
<template v-slot:prepend>
<v-tooltip :text="$t('dialog.back')" 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-data-table>
</v-card-text>
</v-card>
</v-container>
</v-tooltip>
</template>
<v-spacer></v-spacer>
<v-tooltip text="افزودن سند حقوق" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-plus" variant="text" color="success" :to="'/acc/hrm/docs/mod/'"></v-btn>
</template>
</v-tooltip>
</v-toolbar>
<v-text-field v-model="searchValue" prepend-inner-icon="mdi-magnify" density="compact" hide-details :rounded="false"
placeholder="جست و جو ...">
</v-text-field>
<v-data-table :headers="headers" :items="filteredItems" :search="searchValue" :loading="loading"
:header-props="{ class: 'custom-header' }" hover>
<template v-slot:item.actions="{ item }">
<v-tooltip text="مشاهده سند" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon variant="text" color="success" :to="'/acc/hrm/docs/view/' + item.id">
<v-icon>mdi-eye</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn variant="text" size="small" color="error" icon="mdi-menu" v-bind="props" />
</template>
<v-list>
<v-list-item :title="$t('dialog.view')" :to="'/acc/hrm/docs/view/' + item.id">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-eye"></v-icon>
</template>
</v-list-item>
<v-list-item :title="$t('dialog.edit')" :to="'/acc/hrm/docs/mod/' + item.id">
<template v-slot:prepend>
<v-icon icon="mdi-file-edit"></v-icon>
</template>
</v-list-item>
<v-list-item title="صدور سند حسابداری" :to="'/acc/hrm/docs/accounting/' + item.id">
<template v-slot:prepend>
<v-icon color="primary" icon="mdi-file-document-outline"></v-icon>
</template>
</v-list-item>
<v-list-item :title="$t('dialog.delete')" @click="openDeleteDialog(item)">
<template v-slot:prepend>
<v-icon color="deep-orange-accent-4" icon="mdi-trash-can"></v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-data-table>
<!-- دیالوگ تأیید حذف -->
<v-dialog v-model="deleteDialog" max-width="500">
<v-card class="rounded-lg">
<v-card-title class="d-flex align-center pa-4">
<v-icon color="error" size="large" class="ml-2">mdi-alert-circle-outline</v-icon>
<span class="text-h5 font-weight-bold">حذف سند حقوق</span>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-4">
<div class="d-flex flex-column">
<div class="text-subtitle-1 mb-2">آیا مطمئن هستید که میخواهید سند زیر را حذف کنید؟</div>
<v-card variant="outlined" class="mt-2">
<v-card-text>
<div class="d-flex justify-space-between mb-2">
<span class="text-subtitle-2 font-weight-bold">کد سند:</span>
<span>{{ selectedItem?.id?.toLocaleString() }}</span>
</div>
<div class="d-flex justify-space-between mb-2">
<span class="text-subtitle-2 font-weight-bold">تاریخ:</span>
<span>{{ selectedItem?.date }}</span>
</div>
<div class="d-flex justify-space-between mb-2">
<span class="text-subtitle-2 font-weight-bold">کارمند:</span>
<span>{{ selectedItem?.employee }}</span>
</div>
<div class="d-flex justify-space-between">
<span class="text-subtitle-2 font-weight-bold">مبلغ:</span>
<span>{{ selectedItem?.amountRaw?.toLocaleString() }}</span>
</div>
</v-card-text>
</v-card>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn
color="grey-darken-1"
variant="text"
@click="deleteDialog = false"
:disabled="deleteLoading"
>
انصراف
</v-btn>
<v-btn
color="error"
variant="tonal"
@click="confirmDelete"
:loading="deleteLoading"
>
<template v-slot:prepend>
<v-icon>mdi-delete</v-icon>
</template>
حذف سند
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- اسنکبار برای نمایش پیام -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="3000">
{{ snackbar.message }}
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false">
بستن
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
<script setup>
import { ref, onMounted, computed } from 'vue'
import axios from 'axios'
import moment from 'jalali-moment'
export default {
data() {
return {
loading: false,
headers: [
{ title: this.$t('dialog.field.id'), key: 'id' },
{ title: this.$t('dialog.field.date'), key: 'date' },
{ title: this.$t('dialog.field.employee'), key: 'employee' },
{ title: this.$t('dialog.field.amount'), key: 'amount' },
{ title: this.$t('dialog.field.status'), key: 'status' },
{ title: this.$t('dialog.field.actions'), key: 'actions', sortable: false }
],
items: []
const searchValue = ref('')
const loading = ref(true)
const items = ref([])
const deleteDialog = ref(false)
const deleteLoading = ref(false)
const selectedItem = ref(null)
const snackbar = ref({
show: false,
message: '',
color: 'success'
})
const headers = [
{ title: 'عملیات', key: 'actions' },
{ title: 'تاریخ', key: 'date', sortable: true },
{ title: 'ایجاد کننده', key: 'employee', sortable: true },
{ title: 'مبلغ', key: 'amount', sortable: true },
{ title: 'سند حسابداری', key: 'accounting_doc', sortable: true },
{ title: 'توضیحات', key: 'description', sortable: true },
]
const loadData = async () => {
try {
loading.value = true;
const response = await axios.post('/api/hrm/docs/list', {
search: searchValue.value
});
if (response.data.success) {
items.value = response.data.data.map(item => ({
...item,
amount: item.total ? item.total.toLocaleString('fa-IR') : '0',
amountRaw: item.total || 0,
employee: item.creator?.name || 'نامشخص',
status: item.status
}));
} else {
snackbar.value = {
show: true,
message: 'خطا در بارگذاری داده‌ها',
color: 'error'
};
}
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
this.loading = true
try {
const response = await axios.post('/api/hrm/docs/list')
this.items = response.data
} catch (error) {
console.error('Error loading data:', error)
} catch (error) {
console.error('Error loading data:', error);
snackbar.value = {
show: true,
message: 'خطا در بارگذاری داده‌ها',
color: 'error'
};
} finally {
loading.value = false;
}
};
const filteredItems = computed(() => {
return items.value;
});
const openDeleteDialog = (item) => {
selectedItem.value = item
deleteDialog.value = true
}
const confirmDelete = async () => {
try {
deleteLoading.value = true
const response = await axios.post('/api/hrm/docs/delete',{id:selectedItem.value.id})
if (response.data.success) {
const index = items.value.findIndex(item => item.id === selectedItem.value.id)
if (index !== -1) {
items.value.splice(index, 1)
}
deleteDialog.value = false
snackbar.value = {
show: true,
message: 'سند با موفقیت حذف شد',
color: 'success'
}
} else {
snackbar.value = {
show: true,
message: response.data.message || 'خطا در حذف سند',
color: 'error'
}
this.loading = false
}
} catch (error) {
snackbar.value = {
show: true,
message: error.response?.data?.message || 'خطا در ارتباط با سرور',
color: 'error'
}
} finally {
deleteLoading.value = false
}
}
</script>
onMounted(() => {
loadData()
})
</script>
<style scoped>
.v-data-table {
direction: rtl;
}
</style>

View file

@ -25,7 +25,7 @@
<v-row>
<v-col cols="12" sm="6" md="6">
<Hdatepicker v-model="form.date" :label="$t('dialog.hrm.date')"
:rules="[v => !!v || $t('dialog.hrm.required_fields.date')]" required />
:rules="[v => !!v || $t('dialog.hrm.required_fields.date')]" required density="compact" />
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="form.description" :label="$t('dialog.hrm.description')"
@ -131,6 +131,9 @@
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
import Hpersonsearch from '@/components/forms/Hpersonsearch.vue';
import Hnumberinput from '@/components/forms/Hnumberinput.vue';
import axios from 'axios';
import moment from 'jalali-moment';
export default {
components: { Hdatepicker, Hpersonsearch, Hnumberinput },
data() {
@ -150,20 +153,47 @@ export default {
tableItems: [],
}
},
mounted() {
const id = this.$route.params.id
if (id) {
this.isEdit = true
this.loadData(id)
async mounted() {
try {
// دریافت تاریخ فعلی
const response = await axios.get('/api/year/get');
this.form.date = response.data.now;
const id = this.$route.params.id
if (id) {
this.isEdit = true
await this.loadData(id)
}
} catch (error) {
this.errorMessage = error.response?.data?.error || 'خطا در دریافت تاریخ';
this.showError = true;
}
},
methods: {
async loadData(id) {
try {
const response = await this.$axios.post('/api/hrm/docs/get/' + id)
this.form = response.data
const response = await axios.post('/api/hrm/docs/get/' + id)
const data = response.data.data
this.form = {
date: data.date ? moment(data.date, 'jYYYY/jMM/jDD').format('jYYYY/jMM/jDD') : '',
description: data.description || ''
}
this.tableItems = data.items.map(item => ({
person: {
id: item.person.id,
name: item.person.name,
code: item.person.code
},
description: item.description || '',
baseSalary: item.baseSalary || 0,
overtime: item.overtime || 0,
shift: item.shift || 0,
night: item.night || 0
}))
} catch (error) {
this.errorMessage = 'خطا در دریافت اطلاعات';
this.errorMessage = error.response?.data?.error || 'خطا در دریافت اطلاعات';
this.showError = true;
}
},
@ -193,16 +223,35 @@ export default {
},
async save() {
try {
console.log('Starting save process...');
this.loading = true;
const url = this.isEdit ? '/api/hrm/docs/update' : '/api/hrm/docs/insert'
await this.$axios.post(url, this.form)
const url = this.isEdit ? '/api/hrm/docs/update' : '/api/hrm/docs/insert';
const data = {
date: this.form.date,
description: this.form.description,
items: this.tableItems.map(item => ({
person: item.person?.id,
baseSalary: Number(item.baseSalary) || 0,
overtime: Number(item.overtime) || 0,
shift: Number(item.shift) || 0,
night: Number(item.night) || 0,
description: item.description || ''
}))
};
console.log('Sending request to:', url);
console.log('Request data:', data);
const response = await axios.post(url, data);
console.log('Server response:', response);
this.successMessage = this.isEdit ? this.$t('dialog.hrm.edit_success') : this.$t('dialog.hrm.save_success');
this.showSuccess = true;
setTimeout(() => {
this.$router.push('/acc/hrm/docs/list')
}, 1200)
this.$router.push('/acc/hrm/docs/list');
}, 1200);
} catch (error) {
this.errorMessage = this.$t('dialog.hrm.save_error');
console.error('Save error:', error);
this.errorMessage = error.response?.data?.error || this.$t('dialog.hrm.save_error');
this.showError = true;
} finally {
this.loading = false;
@ -211,7 +260,7 @@ export default {
async confirmDelete() {
try {
this.loading = true;
await this.$axios.post('/api/hrm/docs/delete', { id: this.$route.params.id })
await axios.post('/api/hrm/docs/delete', { id: this.$route.params.id })
this.successMessage = 'سند با موفقیت حذف شد';
this.showSuccess = true;
setTimeout(() => {
@ -318,6 +367,14 @@ export default {
z-index: 9999 !important;
}
:deep(.v-date-picker__menu) {
z-index: 9999 !important;
}
:deep(.v-date-picker__menu__content) {
z-index: 9999 !important;
}
.settings-section-card {
height: 100%;
transition: all 0.3s ease;

View file

@ -1,67 +1,164 @@
<template>
<div class="sticky-container">
<v-toolbar color="toolbar" :title="$t('dialog.person_with_det_report')">
<template v-slot:prepend>
<v-tooltip :text="$t('dialog.back')" 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>
<template v-slot:prepend>
<v-tooltip :text="$t('dialog.back')" 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-toolbar>
<v-container fluid>
<v-row>
<v-col cols="12" md="4">
<Hpersonsearch
v-model="selectedPerson"
label="شخص"
:rules="[v => !!v || 'انتخاب شخص الزامی است']"
/>
</v-col>
<v-col cols="12" md="4">
<Hdatepicker
v-model="startDate"
label="تاریخ شروع"
:rules="[v => !!v || 'تاریخ شروع الزامی است']"
/>
</v-col>
<v-col cols="12" md="4">
<Hdatepicker
v-model="endDate"
label="تاریخ پایان"
:rules="[v => !!v || 'تاریخ پایان الزامی است']"
/>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="4">
<Hpersonsearch
v-model="selectedPerson"
label="شخص"
:rules="[v => !!v || 'انتخاب شخص الزامی است']"
/>
</v-col>
<v-col cols="12" md="4">
<Hdatepicker
v-model="startDate"
label="تاریخ شروع"
:rules="[v => !!v || 'تاریخ شروع الزامی است']"
/>
</v-col>
<v-col cols="12" md="4">
<Hdatepicker
v-model="endDate"
label="تاریخ پایان"
:rules="[v => !!v || 'تاریخ پایان الزامی است']"
/>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="12">
<v-data-table-server
v-model:items-per-page="serverOptions.rowsPerPage"
v-model:page="serverOptions.page"
:headers="tableHeaders"
:items="items"
:items-length="totalItems"
:loading="loading"
class="elevation-1"
:items-per-page-options="[5, 10, 20, 50]"
item-value="id"
no-data-text="اطلاعاتی برای نمایش وجود ندارد"
:header-props="{ class: 'custom-header' }"
>
<template v-slot:item.index="{ index }">
{{ (serverOptions.page - 1) * serverOptions.rowsPerPage + index + 1 }}
</template>
<template v-slot:item.actions="{ item }">
<v-btn icon size="small" color="primary">
<v-icon>mdi-eye</v-icon>
</v-btn>
</template>
<template v-slot:item.docType="{ item }">
{{ item.docType || '-' }}
</template>
<template v-slot:item.debit="{ item }">
{{ formatNumber(item.debit) }}
</template>
<template v-slot:item.credit="{ item }">
{{ formatNumber(item.credit) }}
</template>
<template v-slot:item.description="{ item }">
{{ item.description }}
</template>
</v-data-table-server>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
import Hpersonsearch from '@/components/forms/Hpersonsearch.vue'
import Hdatepicker from '@/components/forms/Hdatepicker.vue'
import Hpersonsearch from '@/components/forms/Hpersonsearch.vue';
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
import axios from 'axios';
export default {
name: 'PersonWithDetReport',
components: {
Hpersonsearch,
Hdatepicker
name: 'PersonWithDetReport',
components: {
Hpersonsearch,
Hdatepicker
},
data() {
return {
selectedPerson: null,
startDate: '',
endDate: '',
loading: false,
items: [],
totalItems: 0,
serverOptions: {
page: 1,
rowsPerPage: 10,
sortBy: [],
},
tableHeaders: [
{ text: 'ردیف', value: 'index', align: 'center', sortable: false },
{ text: 'عملیات', value: 'actions', align: 'center', sortable: false },
{ text: 'نوع سند', value: 'docType', align: 'center' },
{ text: 'بدهکار', value: 'debit', align: 'center' },
{ text: 'بستانکار', value: 'credit', align: 'center' },
{ text: 'شرح', value: 'description', align: 'center' },
],
};
},
methods: {
formatNumber(num) {
if (!num) return '0';
return Number(num).toLocaleString('fa-IR');
},
data() {
return {
selectedPerson: null,
startDate: '',
endDate: ''
}
async fetchData() {
if (!this.selectedPerson) {
this.items = [];
this.totalItems = 0;
return;
}
this.loading = true;
try {
const response = await axios.post('/api/persons/listwithdet', {
person: this.selectedPerson.code,
startDate: this.startDate,
endDate: this.endDate,
page: this.serverOptions.page,
itemsPerPage: this.serverOptions.rowsPerPage,
});
this.items = response.data.items || [];
this.totalItems = response.data.total || this.items.length;
} catch (error) {
this.items = [];
this.totalItems = 0;
} finally {
this.loading = false;
}
},
methods: {
// متدهای مورد نیاز گزارش
},
watch: {
selectedPerson: 'fetchData',
startDate: 'fetchData',
endDate: 'fetchData',
serverOptions: {
handler: 'fetchData',
deep: true,
},
mounted() {
// کدهای اجرایی در زمان بارگذاری کامپوننت
},
mounted() {
// بارگذاری اولیه اگر شخص انتخاب شده باشد
if (this.selectedPerson) {
this.fetchData();
}
}
},
};
</script>
<style scoped>
/* استایل‌های مورد نیاز */
.sticky-container {
position: relative;
}
</style>

View file

@ -791,7 +791,22 @@ export default {
try {
// اول تنظیمات را لود میکنیم
this.loadSettings();
// دریافت آیدی کسبوکار فعال از localStorage
const activeBid = localStorage.getItem('activeBid');
if (activeBid) {
try {
const businessRes = await axios.get(`/api/business/get/info/${activeBid}`);
if (businessRes.data && businessRes.data.maliyatafzode) {
// فقط اگر فاکتور جدید است مقدار پیشفرض مالیات را ست کن
if (!this.$route.params.id) {
this.taxPercent = Number(businessRes.data.maliyatafzode);
}
}
} catch (err) {
console.error('خطا در دریافت اطلاعات کسب‌وکار:', err);
}
}
// بررسی وضعیت پیشنویس
this.isNewInvoice = !this.$route.params.id;

View file

@ -41,6 +41,11 @@ export default defineComponent({
recListWindowsState: { submited: false },
notes: { count: 0 },
bid: { legal_name: '', shortlinks: false },
snackbar: {
show: false,
text: '',
color: 'error'
},
item: {
doc: { id: 0, date: null, code: null, des: '', amount: 0, profit: 0, shortLink: null },
relatedDocs: [],
@ -98,6 +103,26 @@ export default defineComponent({
},
},
methods: {
async checkCanEdit() {
try {
const response = await axios.get(`/api/sell/edit/can/${this.$route.params.id}`);
if (response.data.result) {
this.$router.push(`/acc/sell/mod/${this.$route.params.id}`);
} else {
this.snackbar = {
show: true,
text: 'شما مجاز به ویرایش این فاکتور نیستید',
color: 'error'
};
}
} catch (error) {
this.snackbar = {
show: true,
text: 'خطا در بررسی دسترسی',
color: 'error'
};
}
},
loadData() {
this.loading = true;
this.commoditys = [];
@ -173,7 +198,7 @@ export default defineComponent({
</v-tooltip>
</template>
<v-spacer></v-spacer>
<v-btn icon :to="`/acc/sell/mod/${$route.params.id}`">
<v-btn icon @click="checkCanEdit">
<v-icon>mdi-pencil</v-icon>
<v-tooltip activator="parent" location="bottom">ویرایش</v-tooltip>
</v-btn>
@ -335,6 +360,13 @@ export default defineComponent({
</v-window-item>
</v-window>
</v-container>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
timeout="3000"
>
{{ snackbar.text }}
</v-snackbar>
</template>
<style scoped>