Compare commits

..

2 commits

12 changed files with 1077 additions and 25 deletions

View file

@ -167,7 +167,7 @@ class AccountingDocService
'code' => $params['id'],
]);
if (!$salary)
return ['error' => 'حقوق یافت نشد'];
return ['error' => 'تنخواه یافت نشد'];
// Check if we should include preview documents
$includePreview = $params['includePreview'] ?? false;

View file

@ -441,18 +441,23 @@ class BankController extends AbstractController
'توضیحات',
'شرح سند',
'تفصیل',
'طرف حساب‌ها',
'بستانکار',
'بدهکار',
'سال مالی',
]
];
foreach ($transactions as $transaction) {
// استخراج طرف حساب‌ها برای این تراکنش
$counterpartAccounts = $this->getCounterpartAccountsForTransaction($transaction, $bank, $entityManager);
$arrayEntity[] = [
$transaction->getId(),
$transaction->getDoc()->getDate(),
$transaction->getDes(),
$transaction->getDoc()->getDes(),
$transaction->getRef()->getName(),
$counterpartAccounts,
$transaction->getBs(),
$transaction->getBd(),
$transaction->getYear()->getlabel()
@ -466,6 +471,57 @@ class BankController extends AbstractController
return new BinaryFileResponse($filePath);
}
/**
* استخراج طرف حساب‌های مربوط به یک تراکنش
*/
private function getCounterpartAccountsForTransaction($transaction, $bank, EntityManagerInterface $entityManager): string
{
$doc = $transaction->getDoc();
$bankCode = $bank->getCode();
// دریافت تمام ردیف‌های مربوط به این سند
$docRows = $entityManager->getRepository(HesabdariRow::class)
->createQueryBuilder('hr')
->leftJoin('hr.bank', 'ba')
->leftJoin('hr.cashdesk', 'cd')
->leftJoin('hr.salary', 's')
->leftJoin('hr.person', 'p')
->where('hr.doc = :doc')
->setParameter('doc', $doc)
->getQuery()
->getResult();
$accounts = [];
foreach ($docRows as $docRow) {
// بررسی اینکه آیا این ردیف طرف حساب است (نه بانک انتخابی)
$isCounterpart = false;
$accountName = '';
if ($docRow->getBank()) {
if ($docRow->getBank()->getCode() != $bankCode) {
$isCounterpart = true;
$accountName = 'بانک: ' . $docRow->getBank()->getName();
}
} elseif ($docRow->getCashdesk()) {
$isCounterpart = true;
$accountName = 'صندوق: ' . $docRow->getCashdesk()->getName();
} elseif ($docRow->getSalary()) {
$isCounterpart = true;
$accountName = 'تنخواه: ' . $docRow->getSalary()->getName();
} elseif ($docRow->getPerson()) {
$isCounterpart = true;
$accountName = 'شخص: ' . $docRow->getPerson()->getNikename();
}
if ($isCounterpart) {
$amount = $docRow->getBd() > 0 ? $docRow->getBd() : $docRow->getBs();
$accounts[] = $accountName . ' (' . number_format($amount, 0, '.', ',') . ')';
}
}
return implode(' | ', $accounts);
}
#[Route('/api/bank/card/list/print', name: 'app_bank_card_list_print')]
public function app_bank_card_list_print(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
@ -521,6 +577,12 @@ class BankController extends AbstractController
}
}
}
// اضافه کردن طرف حساب‌ها به هر تراکنش
foreach ($transactions as $transaction) {
$transaction->counterpartAccounts = $this->getCounterpartAccountsForTransaction($transaction, $bank, $entityManager);
}
$pid = $provider->createPrint(
$acc['bid'],
$this->getUser(),

View file

@ -491,18 +491,28 @@ class HesabdariController extends AbstractController
]);
}
// Set approval status based on business settings
// وضعیت تایید: اگر autoApprove=true ارسال شده باشد، اجباری تایید شود
$autoApprove = isset($params['autoApprove']) ? (bool)$params['autoApprove'] : null;
$business = $acc['bid'];
if ($business->isRequireTwoStepApproval()) {
// Two-step approval is enabled
if ($autoApprove === true) {
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
} elseif ($autoApprove === false) {
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
} else {
// Two-step approval is disabled - auto approve
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
// پیش‌فرض مطابق تنظیمات کسب‌وکار
if ($business->isRequireTwoStepApproval()) {
$doc->setIsPreview(true);
$doc->setIsApproved(false);
$doc->setApprovedBy(null);
} else {
$doc->setIsPreview(false);
$doc->setIsApproved(true);
$doc->setApprovedBy($this->getUser());
}
}
if (array_key_exists('refData', $params))

View file

@ -18,6 +18,7 @@ use App\Entity\Storeroom;
use App\Entity\StoreroomItem;
use App\Entity\StoreroomTicket;
use App\Service\Explore;
use App\Cog\AccountingDocService;
use App\Cog\PersonService;
use Doctrine\ORM\EntityManagerInterface;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
@ -2926,4 +2927,123 @@ class PersonsController extends AbstractController
return $this->json(['success' => true, 'message' => "$successCount سند پرداخت تایید شد"]);
}
/**
* دریافت طرف حساب‌های مربوط به حساب بانکی
*/
#[Route('/api/person/bank/accounts/list', name: 'app_person_bank_accounts_list')]
public function app_bank_accounts_list(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
AccountingDocService $accountingDocService
): JsonResponse {
$acc = $access->hasRole('getpay');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true) ?? [];
$bankCode = $params['bankCode'] ?? null;
if (!$bankCode) {
return $this->json(['error' => 'کد بانک الزامی است'], 400);
}
// استفاده از AccountingDocService برای دریافت ردیف‌های حسابداری
$searchParams = [
'type' => 'bank',
'id' => $bankCode,
'includePreview' => true // شامل اسناد پیش‌نمایش هم می‌شود
];
$rows = $accountingDocService->searchRows($searchParams, $acc);
if (isset($rows['error'])) {
return $this->json(['error' => $rows['error']], 400);
}
// استخراج طرف حساب‌ها از ردیف‌ها
$accounts = [];
$processedDocs = [];
foreach ($rows as $row) {
$docId = $row['code']; // کد سند
// اگر این سند قبلاً پردازش شده، رد کن
if (in_array($docId, $processedDocs)) {
continue;
}
$processedDocs[] = $docId;
// دریافت سند و ردیف‌های مربوط به آن
$doc = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findOneBy(['code' => $docId]);
if (!$doc) {
continue;
}
$docRows = $entityManager->getRepository(\App\Entity\HesabdariRow::class)
->createQueryBuilder('hr')
->leftJoin('hr.bank', 'ba')
->leftJoin('hr.cashdesk', 'cd')
->leftJoin('hr.salary', 's')
->leftJoin('hr.person', 'p')
->where('hr.doc = :doc')
->setParameter('doc', $doc)
->getQuery()
->getResult();
$docAccounts = [];
foreach ($docRows as $docRow) {
// بررسی اینکه آیا این ردیف طرف حساب است (نه بانک انتخابی)
$isCounterpart = false;
$accountName = '';
$accountType = '';
$amount = 0;
if ($docRow->getBank()) {
if ($docRow->getBank()->getCode() != $bankCode) {
$isCounterpart = true;
$accountName = 'بانک: ' . $docRow->getBank()->getName();
$accountType = 'bank';
}
} elseif ($docRow->getCashdesk()) {
$isCounterpart = true;
$accountName = 'صندوق: ' . $docRow->getCashdesk()->getName();
$accountType = 'cashdesk';
} elseif ($docRow->getSalary()) {
$isCounterpart = true;
$accountName = 'تنخواه: ' . $docRow->getSalary()->getName();
$accountType = 'salary';
} elseif ($docRow->getPerson()) {
$isCounterpart = true;
$accountName = 'شخص: ' . $docRow->getPerson()->getNikename();
$accountType = 'person';
}
if ($isCounterpart) {
$amount = $docRow->getBd() > 0 ? $docRow->getBd() : $docRow->getBs();
$docAccounts[] = [
'name' => $accountName,
'type' => $accountType,
'amount' => $amount,
'formattedAmount' => number_format($amount, 0, '.', ','),
];
}
}
if (!empty($docAccounts)) {
$accounts[] = [
'docId' => $docId,
'date' => $row['date'],
'type' => $row['type'],
'des' => $row['des'],
'accounts' => $docAccounts
];
}
}
return $this->json($accounts);
}
}

View file

@ -26,6 +26,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Entity\BankAccount;
use App\Entity\Cashdesk;
@ -800,11 +801,56 @@ class SellController extends AbstractController
// اولویت با پارامترهای ارسالی است
$printOptions = array_merge($defaultOptions, $params['printOptions'] ?? []);
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
$docRepo = $entityManager->getRepository(HesabdariDoc::class);
$doc = $docRepo->findOneBy([
'bid' => $acc['bid'],
'code' => $params['code'],
'money' => $acc['money']
]);
// Fallback در صورت ناهمخوانی activeMoney
if (!$doc) {
$doc = $docRepo->findOneBy([
'bid' => $acc['bid'],
'code' => $params['code'],
'year' => $acc['year']
]);
}
// Fallback با QueryBuilder بر اساس شناسه BID (عدم تکیه بر شیء)
if (!$doc) {
try {
$doc = $entityManager->createQueryBuilder()
->select('d')
->from(\App\Entity\HesabdariDoc::class, 'd')
->where('d.code = :code')
->andWhere('IDENTITY(d.bid) = :bidId')
->setParameter('code', (string)($params['code'] ?? ''))
->setParameter('bidId', $acc['bid']->getId())
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
} catch (\Exception $e) {
$doc = null;
}
}
// Fallback نهایی فقط بر اساس کد (در صورت وجود چند کسب‌وکار، کنترل امنیت بعدی برقرار است)
if (!$doc) {
try {
$doc = $entityManager->createQueryBuilder()
->select('d')
->from(\App\Entity\HesabdariDoc::class, 'd')
->where('d.code = :code')
->setParameter('code', (string)($params['code'] ?? ''))
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
if ($doc && $doc->getBid()->getId() !== $acc['bid']->getId()) {
// سند پیدا شد ولی متعلق به کسب‌وکار دیگری است
throw $this->createAccessDeniedException();
}
} catch (\Exception $e) {
$doc = null;
}
}
if (!$doc)
throw $this->createNotFoundException();
$person = null;
@ -953,6 +999,10 @@ class SellController extends AbstractController
false,
$printOptions['paper']
);
// اگر چاپ ابری انتخاب شده، فایل را به صف پرینترها اضافه کن
if (!empty($params['printers'])) {
$printers->addFile($pdfPid, $acc, "fastSellInvoice");
}
}
if ($params['posPrint'] == true) {
$pid = $provider->createPrint(
@ -977,9 +1027,23 @@ class SellController extends AbstractController
'showPercentDiscount' => $doc->getDiscountType() === 'percent',
'discountPercent' => $doc->getDiscountPercent()
]),
false
true
);
$printers->addFile($pid, $acc, "fastSellInvoice");
$printers->addFile($pid, $acc, "fastSellPosInvoice");
}
// چاپ قبض صندوق در صورت نیاز
if (!empty($params['posPrintRecp'])) {
$pid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$this->renderView('pdf/posPrinters/cashdesk.html.twig', [
'bid' => $acc['bid'],
'doc' => $doc,
'rows' => $doc->getHesabdariRows(),
]),
true
);
$printers->addFile($pid, $acc, "fastSellCashdesk");
}
return $this->json(['id' => $pdfPid]);
}
@ -1587,4 +1651,452 @@ class SellController extends AbstractController
]);
}
}
#[Route('/api/sell/list/excel', name: 'app_sell_list_excel', methods: ['POST'])]
public function app_sell_list_excel(
Provider $provider,
Request $request,
Access $access,
Log $log,
EntityManagerInterface $entityManager,
Jdate $jdate
): BinaryFileResponse|JsonResponse|StreamedResponse {
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true) ?? [];
$searchTerm = $params['search'] ?? '';
$types = $params['types'] ?? [];
$dateFilter = $params['dateFilter'] ?? 'all';
$sortBy = $params['sortBy'] ?? [];
$queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('d.isPreview, d.isApproved')
->addSelect('u.fullName as submitter')
->addSelect('approver.fullName as approvedByName, approver.id as approvedById, approver.email as approvedByEmail')
->addSelect('l.code as labelCode, l.label as labelLabel')
->from(HesabdariDoc::class, 'd')
->leftJoin('d.submitter', 'u')
->leftJoin('d.approvedBy', 'approver')
->leftJoin('d.InvoiceLabel', 'l')
->leftJoin('d.hesabdariRows', 'r')
->where('d.bid = :bid')
->andWhere('d.year = :year')
->andWhere('d.type = :type')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('type', 'sell')
->setParameter('money', $acc['money']);
// اعمال فیلترهای تاریخ
$today = $jdate->jdate('Y/m/d', time());
if ($dateFilter === 'today') {
$queryBuilder->andWhere('d.date = :today')
->setParameter('today', $today);
} elseif ($dateFilter === 'week') {
$weekStart = $jdate->jdate('Y/m/d', strtotime('-6 days'));
$queryBuilder->andWhere('d.date BETWEEN :weekStart AND :today')
->setParameter('weekStart', $weekStart)
->setParameter('today', $today);
} elseif ($dateFilter === 'month') {
$monthStart = $jdate->jdate('Y/m/01', time());
$queryBuilder->andWhere('d.date BETWEEN :monthStart AND :today')
->setParameter('monthStart', $monthStart)
->setParameter('today', $today);
}
if ($searchTerm) {
$queryBuilder->leftJoin('r.person', 'p')
->andWhere(
$queryBuilder->expr()->orX(
'd.code LIKE :search',
'd.des LIKE :search',
'd.date LIKE :search',
'd.amount LIKE :search',
'p.nikename LIKE :search',
'p.mobile LIKE :search'
)
)
->setParameter('search', "%$searchTerm%");
}
if (!empty($types)) {
$queryBuilder->andWhere('l.code IN (:types)')
->setParameter('types', $types);
}
// اگر آیتم‌های خاصی درخواست شده‌اند
if (array_key_exists('items', $params)) {
$codes = array_map(function($item) { return $item['code']; }, $params['items']);
$queryBuilder->andWhere('d.code IN (:codes)')
->setParameter('codes', $codes);
}
// اعمال مرتب‌سازی
if (!empty($sortBy)) {
foreach ($sortBy as $sort) {
$key = $sort['key'] ?? 'id';
$direction = isset($sort['order']) && strtoupper($sort['order']) === 'DESC' ? 'DESC' : 'ASC';
if ($key === 'profit' || $key === 'receivedAmount') {
continue; // این‌ها توی PHP مرتب می‌شن
} elseif (in_array($key, ['id', 'dateSubmit', 'date', 'type', 'code', 'des', 'amount', 'isPreview', 'isApproved'])) {
$queryBuilder->addOrderBy('d.' . $key, $direction);
} elseif ($key === 'submitter') {
$queryBuilder->addOrderBy('u.fullName', $direction);
} elseif ($key === 'label') {
$queryBuilder->addOrderBy('l.label', $direction);
}
}
} else {
$queryBuilder->orderBy('d.id', 'DESC');
}
$docs = $queryBuilder->getQuery()->getArrayResult();
$dataTemp = [];
foreach ($docs as $doc) {
$item = [
'id' => $doc['id'],
'dateSubmit' => $doc['dateSubmit'],
'date' => $doc['date'],
'type' => $doc['type'],
'code' => $doc['code'],
'des' => $doc['des'],
'amount' => $doc['amount'],
'submitter' => $doc['submitter'],
'label' => $doc['labelCode'] ? [
'code' => $doc['labelCode'],
'label' => $doc['labelLabel']
] : null,
'isPreview' => $doc['isPreview'],
'isApproved' => $doc['isApproved'],
'approvedBy' => $doc['approvedByName'] ? [
'fullName' => $doc['approvedByName'],
'id' => $doc['approvedById'],
'email' => $doc['approvedByEmail']
] : null,
];
$mainRow = $entityManager->getRepository(HesabdariRow::class)
->createQueryBuilder('r')
->where('r.doc = :docId')
->andWhere('r.person IS NOT NULL')
->setParameter('docId', $doc['id'])
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
$item['person'] = $mainRow && $mainRow->getPerson() ? [
'id' => $mainRow->getPerson()->getId(),
'nikename' => $mainRow->getPerson()->getNikename(),
'code' => $mainRow->getPerson()->getCode()
] : null;
// استفاده از SQL خام برای محاسبه پرداختی‌ها
$sql = "
SELECT SUM(rd.amount) as total_pays, COUNT(rd.id) as count_docs
FROM hesabdari_doc rd
JOIN hesabdari_doc_hesabdari_doc rel ON rel.hesabdari_doc_target = rd.id
WHERE rel.hesabdari_doc_source = :sourceDocId
AND rd.bid_id = :bidId
";
$stmt = $entityManager->getConnection()->prepare($sql);
$stmt->bindValue('sourceDocId', $doc['id']);
$stmt->bindValue('bidId', $acc['bid']->getId());
$result = $stmt->executeQuery()->fetchAssociative();
$relatedDocsPays = $result['total_pays'] ?? 0;
$relatedDocsCount = $result['count_docs'] ?? 0;
$item['relatedDocsCount'] = (int) $relatedDocsCount;
$item['relatedDocsPays'] = $relatedDocsPays;
$item['profit'] = $this->calculateProfit($doc['id'], $acc, $entityManager);
$item['discountAll'] = 0;
$item['transferCost'] = 0;
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy(['doc' => $doc]);
foreach ($rows as $row) {
if ($row->getRef()->getCode() == '104') {
$item['discountAll'] = $row->getBd();
} elseif ($row->getRef()->getCode() == '61') {
$item['transferCost'] = $row->getBs();
}
}
$dataTemp[] = $item;
}
// مرتب‌سازی توی PHP برای profit و receivedAmount
if (!empty($sortBy)) {
foreach ($sortBy as $sort) {
$key = $sort['key'] ?? 'id';
$direction = isset($sort['order']) && strtoupper($sort['order']) === 'DESC' ? SORT_DESC : SORT_ASC;
if ($key === 'profit') {
usort($dataTemp, function ($a, $b) use ($direction) {
return $direction === SORT_ASC ? $a['profit'] - $b['profit'] : $b['profit'] - $a['profit'];
});
} elseif ($key === 'receivedAmount') {
usort($dataTemp, function ($a, $b) use ($direction) {
return $direction === SORT_ASC ? $a['relatedDocsPays'] - $b['relatedDocsPays'] : $b['relatedDocsPays'] - $a['relatedDocsPays'];
});
}
}
}
// آماده‌سازی داده‌ها برای Excel
$excelData = [];
$headers = [
'ردیف',
'کد فاکتور',
'تاریخ',
'خریدار',
'وضعیت تایید',
'تاییدکننده',
'تخفیف',
'حمل و نقل',
'مبلغ',
'سود فاکتور',
'پرداختی',
'برچسب',
'شرح'
];
$excelData[] = $headers;
foreach ($dataTemp as $index => $item) {
$excelData[] = [
$index + 1,
$item['code'],
$item['date'],
$item['person'] ? $item['person']['nikename'] : '-',
$item['isApproved'] ? 'تایید شده' : ($item['isPreview'] ? 'در انتظار تایید' : 'تایید شده'),
$item['approvedBy'] ? $item['approvedBy']['fullName'] : '-',
number_format($item['discountAll']),
number_format($item['transferCost']),
number_format($item['amount']),
number_format($item['profit']),
number_format($item['relatedDocsPays']),
$item['label'] ? $item['label']['label'] : '-',
$item['des']
];
}
return new BinaryFileResponse($provider->createExcellFromArray($excelData));
}
#[Route('/api/sell/list/pdf', name: 'app_sell_list_pdf', methods: ['POST'])]
public function app_sell_list_pdf(
Provider $provider,
Request $request,
Access $access,
Log $log,
EntityManagerInterface $entityManager,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true) ?? [];
$searchTerm = $params['search'] ?? '';
$types = $params['types'] ?? [];
$dateFilter = $params['dateFilter'] ?? 'all';
$sortBy = $params['sortBy'] ?? [];
$queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('d.isPreview, d.isApproved')
->addSelect('u.fullName as submitter')
->addSelect('approver.fullName as approvedByName, approver.id as approvedById, approver.email as approvedByEmail')
->addSelect('l.code as labelCode, l.label as labelLabel')
->from(HesabdariDoc::class, 'd')
->leftJoin('d.submitter', 'u')
->leftJoin('d.approvedBy', 'approver')
->leftJoin('d.InvoiceLabel', 'l')
->leftJoin('d.hesabdariRows', 'r')
->where('d.bid = :bid')
->andWhere('d.year = :year')
->andWhere('d.type = :type')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('type', 'sell')
->setParameter('money', $acc['money']);
// اعمال فیلترهای تاریخ
$today = $jdate->jdate('Y/m/d', time());
if ($dateFilter === 'today') {
$queryBuilder->andWhere('d.date = :today')
->setParameter('today', $today);
} elseif ($dateFilter === 'week') {
$weekStart = $jdate->jdate('Y/m/d', strtotime('-6 days'));
$queryBuilder->andWhere('d.date BETWEEN :weekStart AND :today')
->setParameter('weekStart', $weekStart)
->setParameter('today', $today);
} elseif ($dateFilter === 'month') {
$monthStart = $jdate->jdate('Y/m/01', time());
$queryBuilder->andWhere('d.date BETWEEN :monthStart AND :today')
->setParameter('monthStart', $monthStart)
->setParameter('today', $today);
}
if ($searchTerm) {
$queryBuilder->leftJoin('r.person', 'p')
->andWhere(
$queryBuilder->expr()->orX(
'd.code LIKE :search',
'd.des LIKE :search',
'd.date LIKE :search',
'd.amount LIKE :search',
'p.nikename LIKE :search',
'p.mobile LIKE :search'
)
)
->setParameter('search', "%$searchTerm%");
}
if (!empty($types)) {
$queryBuilder->andWhere('l.code IN (:types)')
->setParameter('types', $types);
}
// اگر آیتم‌های خاصی درخواست شده‌اند
if (array_key_exists('items', $params)) {
$codes = array_map(function($item) { return $item['code']; }, $params['items']);
$queryBuilder->andWhere('d.code IN (:codes)')
->setParameter('codes', $codes);
}
// اعمال مرتب‌سازی
if (!empty($sortBy)) {
foreach ($sortBy as $sort) {
$key = $sort['key'] ?? 'id';
$direction = isset($sort['order']) && strtoupper($sort['order']) === 'DESC' ? 'DESC' : 'ASC';
if ($key === 'profit' || $key === 'receivedAmount') {
continue; // این‌ها توی PHP مرتب می‌شن
} elseif (in_array($key, ['id', 'dateSubmit', 'date', 'type', 'code', 'des', 'amount', 'isPreview', 'isApproved'])) {
$queryBuilder->addOrderBy('d.' . $key, $direction);
} elseif ($key === 'submitter') {
$queryBuilder->addOrderBy('u.fullName', $direction);
} elseif ($key === 'label') {
$queryBuilder->addOrderBy('l.label', $direction);
}
}
} else {
$queryBuilder->orderBy('d.id', 'DESC');
}
$docs = $queryBuilder->getQuery()->getArrayResult();
$dataTemp = [];
foreach ($docs as $doc) {
$item = [
'id' => $doc['id'],
'dateSubmit' => $doc['dateSubmit'],
'date' => $doc['date'],
'type' => $doc['type'],
'code' => $doc['code'],
'des' => $doc['des'],
'amount' => $doc['amount'],
'submitter' => $doc['submitter'],
'label' => $doc['labelCode'] ? [
'code' => $doc['labelCode'],
'label' => $doc['labelLabel']
] : null,
'isPreview' => $doc['isPreview'],
'isApproved' => $doc['isApproved'],
'approvedBy' => $doc['approvedByName'] ? [
'fullName' => $doc['approvedByName'],
'id' => $doc['approvedById'],
'email' => $doc['approvedByEmail']
] : null,
];
$mainRow = $entityManager->getRepository(HesabdariRow::class)
->createQueryBuilder('r')
->where('r.doc = :docId')
->andWhere('r.person IS NOT NULL')
->setParameter('docId', $doc['id'])
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
$item['person'] = $mainRow && $mainRow->getPerson() ? [
'id' => $mainRow->getPerson()->getId(),
'nikename' => $mainRow->getPerson()->getNikename(),
'code' => $mainRow->getPerson()->getCode()
] : null;
// استفاده از SQL خام برای محاسبه پرداختی‌ها
$sql = "
SELECT SUM(rd.amount) as total_pays, COUNT(rd.id) as count_docs
FROM hesabdari_doc rd
JOIN hesabdari_doc_hesabdari_doc rel ON rel.hesabdari_doc_target = rd.id
WHERE rel.hesabdari_doc_source = :sourceDocId
AND rd.bid_id = :bidId
";
$stmt = $entityManager->getConnection()->prepare($sql);
$stmt->bindValue('sourceDocId', $doc['id']);
$stmt->bindValue('bidId', $acc['bid']->getId());
$result = $stmt->executeQuery()->fetchAssociative();
$relatedDocsPays = $result['total_pays'] ?? 0;
$relatedDocsCount = $result['count_docs'] ?? 0;
$item['relatedDocsCount'] = (int) $relatedDocsCount;
$item['relatedDocsPays'] = $relatedDocsPays;
$item['profit'] = $this->calculateProfit($doc['id'], $acc, $entityManager);
$item['discountAll'] = 0;
$item['transferCost'] = 0;
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy(['doc' => $doc]);
foreach ($rows as $row) {
if ($row->getRef()->getCode() == '104') {
$item['discountAll'] = $row->getBd();
} elseif ($row->getRef()->getCode() == '61') {
$item['transferCost'] = $row->getBs();
}
}
$dataTemp[] = $item;
}
// مرتب‌سازی توی PHP برای profit و receivedAmount
if (!empty($sortBy)) {
foreach ($sortBy as $sort) {
$key = $sort['key'] ?? 'id';
$direction = isset($sort['order']) && strtoupper($sort['order']) === 'DESC' ? SORT_DESC : SORT_ASC;
if ($key === 'profit') {
usort($dataTemp, function ($a, $b) use ($direction) {
return $direction === SORT_ASC ? $a['profit'] - $b['profit'] : $b['profit'] - $a['profit'];
});
} elseif ($key === 'receivedAmount') {
usort($dataTemp, function ($a, $b) use ($direction) {
return $direction === SORT_ASC ? $a['relatedDocsPays'] - $b['relatedDocsPays'] : $b['relatedDocsPays'] - $a['relatedDocsPays'];
});
}
}
}
$html = $this->renderView('pdf/sell_list.html.twig', [
'items' => $dataTemp,
'bid' => $acc['bid'],
'currentPage' => 1,
'totalPages' => 1,
'totalItems' => count($dataTemp),
'page_title' => 'لیست فاکتورهای فروش'
]);
$pdfPid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$html,
false,
'A4-L'
);
return $this->json(['id' => $pdfPid]);
}
}

View file

@ -50,6 +50,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>
<td class="center item">بستانکار</td>
<td class="center item">سال مالی</td>
@ -66,6 +67,7 @@
<td class="center item">{{ item.des }}</td>
<td class="center item">{{ item.doc.des }}</td>
<td class="center item">{{ item.ref.name }}</td>
<td class="center item">{{ item.counterpartAccounts }}</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>

View file

@ -0,0 +1,53 @@
{% extends "pdf/base.html.twig" %}
{% block body %}
<div style="width:100%;margin-top:5px;text-align:center;">
<table style="width:100%;">
<tbody>
<tr class="stimol" style="text-align: center; background-color: grey; text-color: white">
<td style="width: 35px;">ردیف</td>
<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>
<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>
<td class="center item">برچسب</td>
<td class="center item">شرح</td>
</tr>
{% for item in items %}
<tr class="stimol {% if loop.index is even%}bg-dark text-light{% endif%}">
<td class="center item">{{ loop.index + ((currentPage - 1) * 10) }}</td>
<td class="center item">{{ item.code }}</td>
<td class="center item">{{ item.date }}</td>
<td class="center item">{{ item.person ? item.person.nikename : '-' }}</td>
<td class="center item">
{% if item.isApproved %}
تایید شده
{% elseif item.isPreview %}
در انتظار تایید
{% else %}
تایید شده
{% endif %}
</td>
<td class="center item">{{ item.approvedBy ? item.approvedBy.fullName : '-' }}</td>
<td class="center item">{{ item.discountAll | number_format }}</td>
<td class="center item">{{ item.transferCost | number_format }}</td>
<td class="center item">{{ item.amount | number_format }}</td>
<td class="center item">{{ item.profit | number_format }}</td>
<td class="center item">{{ item.relatedDocsPays | number_format }}</td>
<td class="center item">{{ item.label ? item.label.label : '-' }}</td>
<td class="center item">{{ item.des }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div style="margin-top: 20px; text-align: left;">
<small>صفحه {{ currentPage }} از {{ totalPages }} - تعداد کل: {{ totalItems }}</small>
</div>
</div>
{% endblock %}

View file

@ -191,6 +191,19 @@
{{ item.settlement }}
</v-chip>
</template>
<template v-slot:item.accounts="{ item }">
<div v-for="account in item.accounts" :key="account.name" class="mb-1">
<v-chip
:color="getAccountColor(account.type)"
size="small"
variant="outlined"
class="me-1"
>
{{ account.name }}
<span class="ms-1">({{ account.formattedAmount }})</span>
</v-chip>
</div>
</template>
<template v-slot:item.balance="{ item }">
<span :class="{
'text-success': -item.balance > 0,
@ -234,6 +247,7 @@ export default {
{ title: this.$t('dialog.invoice_num'), key: "code", align: "center", sortable: true },
{ title: this.$t('dialog.date'), key: "date", align: "center", sortable: true },
{ title: this.$t('app.body'), key: "des", align: "center" },
{ title: 'طرف حساب‌ها', key: "accounts", align: "center", sortable: false },
{ title: this.$t('pages.bank_card.detail'), key: "ref", align: "center", sortable: true },
{ title: this.$t('pages.bank_card.deposit'), key: "bd", align: "center", sortable: true },
{ title: this.$t('pages.bank_card.withdrawal'), key: "bs", align: "center", sortable: true },
@ -293,10 +307,34 @@ export default {
this.items.forEach((item) => {
item.bs = this.$filters.formatNumber(item.bs)
item.bd = this.$filters.formatNumber(item.bd)
item.accounts = []; // Initialize accounts array
})
this.loadCounterpartAccounts(id);
this.loading = false;
});
},
loadCounterpartAccounts(bankCode) {
axios.post('/api/person/bank/accounts/list', {
bankCode: bankCode
}).then((response) => {
// Group accounts by document code
const accountsByDoc = {};
response.data.forEach(doc => {
accountsByDoc[doc.docId] = doc.accounts;
});
// Add accounts to items
this.items.forEach(item => {
item.accounts = accountsByDoc[item.code] || [];
});
}).catch((error) => {
console.error('Error loading counterpart accounts:', error);
// Set empty accounts array for all items
this.items.forEach(item => {
item.accounts = [];
});
});
},
getTypeRoute(type, code) {
const routes = {
sell: '/acc/sell/view/',
@ -347,6 +385,20 @@ export default {
};
return labels[type] || type;
},
getAccountColor(type) {
switch (type) {
case 'bank':
return 'primary';
case 'cashdesk':
return 'success';
case 'salary':
return 'warning';
case 'person':
return 'info';
default:
return 'grey';
}
},
excellOutput(AllItems = true) {
if (AllItems) {
this.loading = true;

View file

@ -154,7 +154,7 @@
</div>
<div class="col-sm-12 col-md-12">
<small class="mb-2">مبلغ</small>
<money3 @change="calc()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
<money3 @change="calcFromAccount()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
</money3>
</div>
</div>
@ -203,7 +203,7 @@
</div>
<div class="col-sm-12 col-md-12">
<small class="mb-2">مبلغ</small>
<money3 @change="calc()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
<money3 @change="calcFromAccount()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
</money3>
</div>
</div>
@ -252,7 +252,7 @@
</div>
<div class="col-sm-12 col-md-12">
<small class="mb-2">مبلغ</small>
<money3 @change="calc()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
<money3 @change="calcFromAccount()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
</money3>
</div>
</div>
@ -417,9 +417,69 @@ export default {
side = parseInt(side) + parseInt(item.amount);
});
this.balance = parseInt(this.sum) - parseInt(side);
// Auto-sync amounts when there's only one person and one account side
this.autoSyncAmounts();
this.funcCanSubmit();
},
autoSyncAmounts() {
// Check if there's exactly one person and one account side
const totalPersons = this.persons.length;
const totalBanks = this.banks.length;
const totalCashdesks = this.cashdesks.length;
const totalSalarys = this.salarys.length;
const totalAccountSides = totalBanks + totalCashdesks + totalSalarys;
// Only auto-sync if there's exactly one person and one account side
if (totalPersons === 1 && totalAccountSides === 1) {
const personAmount = parseInt(this.persons[0].amount) || 0;
// Sync to the single account side
if (totalBanks === 1) {
this.banks[0].amount = personAmount;
} else if (totalCashdesks === 1) {
this.cashdesks[0].amount = personAmount;
} else if (totalSalarys === 1) {
this.salarys[0].amount = personAmount;
}
}
},
autoSyncFromAccount() {
// Check if there's exactly one person and one account side
const totalPersons = this.persons.length;
const totalBanks = this.banks.length;
const totalCashdesks = this.cashdesks.length;
const totalSalarys = this.salarys.length;
const totalAccountSides = totalBanks + totalCashdesks + totalSalarys;
// Only auto-sync if there's exactly one person and one account side
if (totalPersons === 1 && totalAccountSides === 1) {
let accountAmount = 0;
// Get amount from the single account side
if (totalBanks === 1) {
accountAmount = parseInt(this.banks[0].amount) || 0;
} else if (totalCashdesks === 1) {
accountAmount = parseInt(this.cashdesks[0].amount) || 0;
} else if (totalSalarys === 1) {
accountAmount = parseInt(this.salarys[0].amount) || 0;
}
// Sync to the person
this.persons[0].amount = accountAmount;
}
},
calcFromAccount() {
// First sync from account to person
this.autoSyncFromAccount();
// Then run normal calculation
this.calc();
},
funcCanSubmit() {
//check form can submit
if (

View file

@ -153,7 +153,7 @@
</div>
<div class="col-sm-12 col-md-12">
<small class="mb-2">مبلغ</small>
<money3 @change="calc()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
<money3 @change="calcFromAccount()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
</money3>
</div>
</div>
@ -202,7 +202,7 @@
</div>
<div class="col-sm-12 col-md-12">
<small class="mb-2">مبلغ</small>
<money3 @change="calc()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
<money3 @change="calcFromAccount()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
</money3>
</div>
</div>
@ -251,7 +251,7 @@
</div>
<div class="col-sm-12 col-md-12">
<small class="mb-2">مبلغ</small>
<money3 @change="calc()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
<money3 @change="calcFromAccount()" class="form-control" v-model="item.amount" v-bind="currencyConfig">
</money3>
</div>
</div>
@ -416,9 +416,69 @@ export default {
side = parseInt(side) + parseInt(item.amount);
});
this.balance = parseInt(this.sum) - parseInt(side);
// Auto-sync amounts when there's only one person and one account side
this.autoSyncAmounts();
this.funcCanSubmit();
},
autoSyncAmounts() {
// Check if there's exactly one person and one account side
const totalPersons = this.persons.length;
const totalBanks = this.banks.length;
const totalCashdesks = this.cashdesks.length;
const totalSalarys = this.salarys.length;
const totalAccountSides = totalBanks + totalCashdesks + totalSalarys;
// Only auto-sync if there's exactly one person and one account side
if (totalPersons === 1 && totalAccountSides === 1) {
const personAmount = parseInt(this.persons[0].amount) || 0;
// Sync to the single account side
if (totalBanks === 1) {
this.banks[0].amount = personAmount;
} else if (totalCashdesks === 1) {
this.cashdesks[0].amount = personAmount;
} else if (totalSalarys === 1) {
this.salarys[0].amount = personAmount;
}
}
},
autoSyncFromAccount() {
// Check if there's exactly one person and one account side
const totalPersons = this.persons.length;
const totalBanks = this.banks.length;
const totalCashdesks = this.cashdesks.length;
const totalSalarys = this.salarys.length;
const totalAccountSides = totalBanks + totalCashdesks + totalSalarys;
// Only auto-sync if there's exactly one person and one account side
if (totalPersons === 1 && totalAccountSides === 1) {
let accountAmount = 0;
// Get amount from the single account side
if (totalBanks === 1) {
accountAmount = parseInt(this.banks[0].amount) || 0;
} else if (totalCashdesks === 1) {
accountAmount = parseInt(this.cashdesks[0].amount) || 0;
} else if (totalSalarys === 1) {
accountAmount = parseInt(this.salarys[0].amount) || 0;
}
// Sync to the person
this.persons[0].amount = accountAmount;
}
},
calcFromAccount() {
// First sync from account to person
this.autoSyncFromAccount();
// Then run normal calculation
this.calc();
},
funcCanSubmit() {
//check form can submit
if (

View file

@ -43,7 +43,9 @@ export default defineComponent({
canPdf: true,
canPrint: true,
canPrintCashdeskRecp: false,
canPos: false,
update: 0,
printOptions: {},
commodity: [],
selectedCommodity: null,
tempID: '',
@ -70,7 +72,8 @@ export default defineComponent({
units: [],
persons: [],
person: {
nikename: ''
nikename: '',
id: null
},
cashdesks: [],
cashdesk: null,
@ -204,9 +207,7 @@ export default defineComponent({
});
axios.post("/api/printers/options/info").then((response) => {
this.loading = false;
this.canPdf = response.data.fastsell.pdf;
this.canPrintCashdeskRecp = response.data.fastsell.cashdeskTicket;
this.canPrint = response.data.fastsell.invoice;
this.printOptions = response.data.sell;
});
},
save() {
@ -240,7 +241,7 @@ export default defineComponent({
});
if (canAdd) {
this.loading = true;
let outItems = [
let outItems: any[] = [
...this.data.items
];
//save data
@ -261,7 +262,8 @@ export default defineComponent({
date: this.data.date,
des: this.data.des,
rows: outItems,
update: ''
update: '',
autoApprove: true
}).then((response) => {
this.loading = false;
if (response.data.result == '1') {
@ -270,9 +272,10 @@ export default defineComponent({
axios.post('/api/sell/print/invoice', {
code: this.update,
pdf: this.canPdf,
posPrint: this.canPrint,
printers: this.canPrint,
posPrint: this.canPos,
posPrintRecp: this.canPrintCashdeskRecp,
printers: this.canPdf
printOptions: this.printOptions
}).then((response) => {
if (this.canPdf) {
this.printID = response.data.id;
@ -305,6 +308,7 @@ export default defineComponent({
des: 'دریافت وجه فاکتور',
rows: outItems,
update: '',
autoApprove: true,
related: response.data.doc.code
}).then((response) => {
if (response.data.result == '4') {
@ -625,6 +629,15 @@ export default defineComponent({
</label>
</span>
</div>
<div class="col-sm-12 col-md-4">
<span class="form-check form-switch form-check-inline">
<input :disabled="this.loading" v-model="canPos" class="form-check-input" type="checkbox">
<label class="form-check-label">
<i class="fa-solid fa-receipt me-1"></i>
صورت حساب POS
</label>
</span>
</div>
<div class="col-sm-12 col-md-4">
<span class="form-check form-switch form-check-inline">
<input :disabled="this.loading" v-model="canPdf" class="form-check-input" type="checkbox">

View file

@ -20,6 +20,16 @@
<v-btn v-bind="props" icon="mdi-delete" color="danger" @click="deleteItems()"></v-btn>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.export_excel')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-file-excel" color="green" @click="exportToExcel()" :loading="excelLoading"></v-btn>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.export_pdf')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-file-pdf-box" color="red" @click="exportToPdf()" :loading="pdfLoading"></v-btn>
</template>
</v-tooltip>
<v-tooltip v-if="isPluginActive('taxsettings')" text="ارسال گروهی به کارپوشه مودیان" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-cloud-upload" color="orange" @click="sendBulkToTaxSystem()"
@ -414,6 +424,8 @@ export default defineComponent({
types: [],
loading: false,
bulkLoading: false,
excelLoading: false,
pdfLoading: false,
items: [],
itemsApproved: [],
itemsPending: [],
@ -1069,6 +1081,102 @@ export default defineComponent({
}
}
},
async exportToExcel() {
this.excelLoading = true;
try {
const params = {
search: this.searchValue,
types: this.types.filter(t => t.checked).map(t => t.code),
dateFilter: this.dateFilter,
sortBy: this.serverOptions.sortBy,
};
// اگر آیتمهای خاصی انتخاب شدهاند، فقط آنها را export کن
if (this.itemsSelected.length > 0) {
params.items = this.itemsSelected.map(code => ({ code }));
}
const response = await axios.post('/api/sell/list/excel', params, {
responseType: 'blob'
});
const fileURL = window.URL.createObjectURL(new Blob([response.data]));
const fileLink = document.createElement('a');
fileLink.href = fileURL;
fileLink.setAttribute('download', `لیست_فاکتورهای_فروش_${new Date().toLocaleDateString('fa-IR')}.xlsx`);
document.body.appendChild(fileLink);
fileLink.click();
document.body.removeChild(fileLink);
window.URL.revokeObjectURL(fileURL);
Swal.fire({
text: 'فایل اکسل با موفقیت دانلود شد',
icon: 'success',
confirmButtonText: 'قبول'
});
} catch (error) {
console.error('Error exporting to Excel:', error);
Swal.fire({
text: 'خطا در ایجاد فایل اکسل: ' + (error.response?.data?.message || error.message),
icon: 'error',
confirmButtonText: 'قبول'
});
} finally {
this.excelLoading = false;
}
},
async exportToPdf() {
this.pdfLoading = true;
try {
const params = {
search: this.searchValue,
types: this.types.filter(t => t.checked).map(t => t.code),
dateFilter: this.dateFilter,
sortBy: this.serverOptions.sortBy,
};
// اگر آیتمهای خاصی انتخاب شدهاند، فقط آنها را export کن
if (this.itemsSelected.length > 0) {
params.items = this.itemsSelected.map(code => ({ code }));
}
const response = await axios.post('/api/sell/list/pdf', params);
if (response.data && response.data.id) {
const pdfResponse = await axios({
method: 'get',
url: '/front/print/' + response.data.id,
responseType: 'arraybuffer'
});
const fileURL = window.URL.createObjectURL(new Blob([pdfResponse.data]));
const fileLink = document.createElement('a');
fileLink.href = fileURL;
fileLink.setAttribute('download', `لیست_فاکتورهای_فروش_${new Date().toLocaleDateString('fa-IR')}.pdf`);
document.body.appendChild(fileLink);
fileLink.click();
document.body.removeChild(fileLink);
window.URL.revokeObjectURL(fileURL);
Swal.fire({
text: 'فایل PDF با موفقیت دانلود شد',
icon: 'success',
confirmButtonText: 'قبول'
});
} else {
throw new Error('خطا در دریافت شناسه چاپ');
}
} catch (error) {
console.error('Error exporting to PDF:', error);
Swal.fire({
text: 'خطا در ایجاد فایل PDF: ' + (error.response?.data?.message || error.message),
icon: 'error',
confirmButtonText: 'قبول'
});
} finally {
this.pdfLoading = false;
}
},
},
created() {
this.loadColumnSettings();