1010 lines
43 KiB
PHP
1010 lines
43 KiB
PHP
<?php
|
|
|
|
namespace Modules\Accounting\Http\Controllers;
|
|
|
|
use App\Business;
|
|
use App\HoldingEntity;
|
|
use App\Transaction;
|
|
use App\Utils\ModuleUtil;
|
|
use App\Utils\Util;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Routing\Controller;
|
|
use Modules\Accounting\Domain\FinancialPostingRequest;
|
|
use Modules\Accounting\Entities\AccountingAccount;
|
|
use Modules\Accounting\Entities\AccountingAccTransMapping;
|
|
use Modules\Accounting\Entities\AccountingAnalyticalDimension;
|
|
use Modules\Accounting\Entities\AccountingBankReconciliation;
|
|
use Modules\Accounting\Entities\AccountingCheque;
|
|
use Modules\Accounting\Entities\AccountingConsolidationRun;
|
|
use Modules\Accounting\Entities\AccountingDepreciationRun;
|
|
use Modules\Accounting\Entities\AccountingFiscalPeriod;
|
|
use Modules\Accounting\Entities\AccountingInstallmentPlan;
|
|
use Modules\Accounting\Entities\AccountingInstallmentSchedule;
|
|
use Modules\Accounting\Entities\AccountingBankLoan;
|
|
use Modules\Accounting\Entities\AccountingBankLoanPayment;
|
|
use Modules\Accounting\Entities\AccountingInsurancePremium;
|
|
use Modules\Accounting\Entities\AccountingPayrollTaxBracket;
|
|
use Modules\Accounting\Services\AccountingHealthService;
|
|
use Modules\Accounting\Services\BankLoanService;
|
|
use Modules\Accounting\Services\BankReconciliationService;
|
|
use Modules\Accounting\Services\ChequeService;
|
|
use Modules\Accounting\Services\ConsolidationService;
|
|
use Modules\Accounting\Services\DeferredDocumentsService;
|
|
use Modules\Accounting\Services\DepreciationService;
|
|
use Modules\Accounting\Services\ExchangeRateService;
|
|
use Modules\Accounting\Services\FinancialPostingService;
|
|
use Modules\Accounting\Services\HoldingEntityResolutionService;
|
|
use Modules\Accounting\Services\InstallmentBridgeService;
|
|
use Modules\Accounting\Services\InstallmentService;
|
|
use Modules\Accounting\Services\InsurancePremiumService;
|
|
use Modules\Accounting\Services\JournalApprovalService;
|
|
use Modules\Accounting\Services\LocationDefaultMappingService;
|
|
use Modules\Accounting\Services\OpeningBalanceService;
|
|
use Modules\Accounting\Services\PayrollTaxTableService;
|
|
use Modules\Accounting\Services\PaymentAccountGlLinkService;
|
|
use Modules\Accounting\Services\PeriodCloseService;
|
|
use Modules\Accounting\Utils\AccountingUtil;
|
|
use Yajra\DataTables\Facades\DataTables;
|
|
|
|
class HoldingAccountingController extends Controller
|
|
{
|
|
public function __construct(
|
|
protected ModuleUtil $moduleUtil,
|
|
protected Util $util
|
|
) {}
|
|
|
|
protected function authorizeAccounting(string $permission = 'accounting.access_accounting_module'): void
|
|
{
|
|
$business_id = request()->session()->get('user.business_id');
|
|
if (! (auth()->user()->can('superadmin') ||
|
|
$this->moduleUtil->hasThePermissionInSubscription($business_id, 'accounting_module')) ||
|
|
! auth()->user()->can($permission)) {
|
|
abort(403, 'Unauthorized action.');
|
|
}
|
|
}
|
|
|
|
// --- Health ---
|
|
public function health(AccountingHealthService $healthService)
|
|
{
|
|
$this->authorizeAccounting('accounting.view_reports');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$health = $healthService->assess($business_id);
|
|
|
|
return view('accounting::holding.health', compact('health'));
|
|
}
|
|
|
|
// --- Fiscal periods ---
|
|
public function fiscalPeriods()
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$periods = AccountingFiscalPeriod::where('business_id', $business_id)->orderByDesc('start_date')->get();
|
|
$accounts = AccountingAccount::forDropdown($business_id, false);
|
|
|
|
return view('accounting::holding.fiscal_periods', compact('periods', 'accounts'));
|
|
}
|
|
|
|
public function storeFiscalPeriod(Request $request, PeriodCloseService $periodCloseService)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$request->validate([
|
|
'name' => 'required|string|max:191',
|
|
'start_date' => 'required|date',
|
|
'end_date' => 'required|date|after_or_equal:start_date',
|
|
]);
|
|
|
|
$periodCloseService->createPeriod(
|
|
$business_id,
|
|
$request->name,
|
|
$request->start_date,
|
|
$request->end_date
|
|
);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]);
|
|
}
|
|
|
|
public function closeFiscalPeriod($id, Request $request, PeriodCloseService $periodCloseService)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
|
|
$periodCloseService->closeAndRollForward(
|
|
$business_id,
|
|
(int) $id,
|
|
$user_id,
|
|
$request->input('retained_earnings_account_id'),
|
|
(bool) $request->input('create_next_period')
|
|
);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.period_closed_success')]);
|
|
}
|
|
|
|
// --- Cheques ---
|
|
public function cheques()
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
|
|
if (request()->ajax()) {
|
|
$accounts = AccountingAccount::forDropdown($business_id, false);
|
|
$cheques = AccountingCheque::where('business_id', $business_id)->orderByDesc('id');
|
|
|
|
return DataTables::of($cheques)
|
|
->addColumn('action', function ($row) use ($accounts) {
|
|
return view('accounting::holding.partials.cheque_actions', [
|
|
'row' => $row,
|
|
'accounts' => $accounts,
|
|
])->render();
|
|
})
|
|
->editColumn('amount', function ($row) {
|
|
return $this->util->num_f($row->amount, true);
|
|
})
|
|
->rawColumns(['action'])
|
|
->make(true);
|
|
}
|
|
|
|
$accounts = AccountingAccount::forDropdown($business_id, false);
|
|
|
|
return view('accounting::holding.cheques', compact('accounts'));
|
|
}
|
|
|
|
public function storeCheque(Request $request, ChequeService $chequeService)
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
|
|
$request->validate([
|
|
'direction' => 'required|in:received,paid',
|
|
'cheque_number' => 'required|string|max:100',
|
|
'amount' => 'required|numeric|min:0.01',
|
|
'issue_date' => 'required|date',
|
|
]);
|
|
|
|
$chequeService->register($request->only([
|
|
'direction', 'cheque_number', 'bank_name', 'amount', 'issue_date', 'due_date',
|
|
'contact_id', 'accounting_account_id', 'counter_account_id', 'note',
|
|
]), $business_id, $user_id);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]);
|
|
}
|
|
|
|
public function transitionCheque($id, Request $request, ChequeService $chequeService)
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
$cheque = AccountingCheque::where('business_id', $business_id)->findOrFail($id);
|
|
|
|
$request->validate([
|
|
'status' => 'required|in:deposited,cleared,bounced,endorsed,cancelled,pending',
|
|
'counter_account_id' => 'nullable|integer',
|
|
]);
|
|
|
|
try {
|
|
$chequeService->transition(
|
|
$cheque,
|
|
$request->input('status'),
|
|
$user_id,
|
|
$request->input('counter_account_id') ? (int) $request->input('counter_account_id') : null
|
|
);
|
|
} catch (\RuntimeException $e) {
|
|
return redirect()->back()->with('status', ['success' => 0, 'msg' => $e->getMessage()]);
|
|
}
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.updated_success')]);
|
|
}
|
|
|
|
// --- Installments ---
|
|
public function installments(InstallmentBridgeService $bridgeService, InstallmentService $installmentService)
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$plans = AccountingInstallmentPlan::where('business_id', $business_id)
|
|
->with('schedule')
|
|
->orderByDesc('id')
|
|
->paginate(20);
|
|
$eligibleSells = $bridgeService->eligibleSells($business_id);
|
|
$accounts = AccountingAccount::forDropdown($business_id, false);
|
|
|
|
return view('accounting::holding.installments', compact('plans', 'eligibleSells', 'accounts', 'installmentService'));
|
|
}
|
|
|
|
public function createInstallmentFromSell($transactionId, InstallmentBridgeService $bridgeService)
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
|
|
$transaction = Transaction::where('business_id', $business_id)
|
|
->where('type', 'sell')
|
|
->findOrFail($transactionId);
|
|
|
|
$installmentCount = (int) request()->input('installment_count', $transaction->pay_term_number ?: 3);
|
|
|
|
$plan = $bridgeService->createFromSell($transaction, $user_id, $installmentCount);
|
|
|
|
if (! $plan) {
|
|
return redirect()->back()->with('status', ['success' => 0, 'msg' => __('accounting::lang.installment_create_failed')]);
|
|
}
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.installment_created_from_sell')]);
|
|
}
|
|
|
|
public function storeInstallmentPlan(Request $request, InstallmentService $installmentService)
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
|
|
$request->validate([
|
|
'transaction_id' => 'required|integer',
|
|
'principal_amount' => 'required|numeric|min:0.01',
|
|
'installment_count' => 'required|integer|min:1',
|
|
'start_date' => 'required|date',
|
|
]);
|
|
|
|
$installmentService->createPlan($request->all(), $business_id, $user_id);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]);
|
|
}
|
|
|
|
public function payInstallment($id, Request $request, InstallmentService $installmentService)
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$schedule = AccountingInstallmentSchedule::with('plan')->findOrFail($id);
|
|
|
|
$installmentService->recordPayment(
|
|
$schedule,
|
|
(float) $request->input('amount'),
|
|
request()->session()->get('user.id'),
|
|
(int) $request->input('debit_account_id'),
|
|
(int) $request->input('credit_account_id')
|
|
);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.updated_success')]);
|
|
}
|
|
|
|
// --- Depreciation ---
|
|
public function depreciation()
|
|
{
|
|
$this->authorizeAccounting('accounting.add_journal');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$runs = AccountingDepreciationRun::where('business_id', $business_id)->orderByDesc('id')->paginate(20);
|
|
$accounts = AccountingAccount::forDropdown($business_id, false);
|
|
|
|
return view('accounting::holding.depreciation', compact('runs', 'accounts'));
|
|
}
|
|
|
|
public function storeDepreciation(Request $request, DepreciationService $depreciationService)
|
|
{
|
|
$this->authorizeAccounting('accounting.add_journal');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
|
|
$depreciationService->createRun($request->all(), $business_id, $user_id);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]);
|
|
}
|
|
|
|
public function postDepreciation($id, DepreciationService $depreciationService)
|
|
{
|
|
$this->authorizeAccounting('accounting.add_journal');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
$run = AccountingDepreciationRun::where('business_id', $business_id)->findOrFail($id);
|
|
$depreciationService->postRun($run, $user_id);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.depreciation_posted')]);
|
|
}
|
|
|
|
public function batchDepreciation(Request $request, DepreciationService $depreciationService)
|
|
{
|
|
$this->authorizeAccounting('accounting.add_journal');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
|
|
$request->validate([
|
|
'period_date' => 'required|date',
|
|
'expense_account_id' => 'required|integer',
|
|
'accumulated_account_id' => 'required|integer',
|
|
]);
|
|
|
|
$result = $depreciationService->batchFromAssets(
|
|
$business_id,
|
|
$user_id,
|
|
$request->period_date,
|
|
(int) $request->expense_account_id,
|
|
(int) $request->accumulated_account_id
|
|
);
|
|
|
|
return redirect()->back()->with('status', [
|
|
'success' => 1,
|
|
'msg' => __('accounting::lang.depreciation_batch_done', ['created' => $result['created'], 'posted' => $result['posted']]),
|
|
]);
|
|
}
|
|
|
|
// --- Bank reconciliation ---
|
|
public function reconciliations()
|
|
{
|
|
$this->authorizeAccounting('accounting.view_reports');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$reconciliations = AccountingBankReconciliation::where('business_id', $business_id)->orderByDesc('id')->paginate(20);
|
|
$accounts = AccountingAccount::forDropdown($business_id, false);
|
|
|
|
return view('accounting::holding.reconciliations', compact('reconciliations', 'accounts'));
|
|
}
|
|
|
|
public function storeReconciliation(Request $request, BankReconciliationService $bankReconciliationService)
|
|
{
|
|
$this->authorizeAccounting('accounting.view_reports');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
|
|
$request->validate([
|
|
'accounting_account_id' => 'required|integer',
|
|
'statement_date' => 'required|date',
|
|
'statement_balance' => 'required|numeric',
|
|
]);
|
|
|
|
$bankReconciliationService->create($business_id, $request->all(), $user_id);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]);
|
|
}
|
|
|
|
public function completeReconciliation($id, Request $request, BankReconciliationService $bankReconciliationService)
|
|
{
|
|
$this->authorizeAccounting('accounting.view_reports');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$reconciliation = AccountingBankReconciliation::where('business_id', $business_id)->findOrFail($id);
|
|
$bankReconciliationService->complete($reconciliation, $request->input('matched_items', []));
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.reconciliation_completed')]);
|
|
}
|
|
|
|
public function syncEntityTagging(HoldingEntityResolutionService $entityResolution)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
|
|
$updated = $entityResolution->backfillUntagged($business_id);
|
|
|
|
return redirect()->back()->with('status', [
|
|
'success' => 1,
|
|
'msg' => __('accounting::lang.entity_tagging_synced', ['count' => $updated]),
|
|
]);
|
|
}
|
|
|
|
// --- Consolidation ---
|
|
public function consolidation(ConsolidationService $consolidationService)
|
|
{
|
|
$this->authorizeAccounting('accounting.view_reports');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$runs = AccountingConsolidationRun::where('business_id', $business_id)->orderByDesc('id')->paginate(20);
|
|
$entities = HoldingEntity::where('business_id', $business_id)
|
|
->where('is_active', true)
|
|
->orderBy('name')
|
|
->get(['id', 'name', 'meta']);
|
|
$defaults = $this->buildConsolidationDefaults($business_id, $entities, null);
|
|
$readiness = $consolidationService->readiness($business_id);
|
|
|
|
return view('accounting::holding.consolidation', compact('runs', 'entities', 'defaults', 'readiness'));
|
|
}
|
|
|
|
public function exchangeRates(ExchangeRateService $exchangeRateService, AccountingUtil $accountingUtil)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$rates = $exchangeRateService->listRates($business_id);
|
|
$business = Business::with('currency')->find($business_id);
|
|
$base_currency_code = strtoupper((string) optional($business->currency)->code ?: 'IRR');
|
|
$accounting_settings = $accountingUtil->getAccountingSettings($business_id);
|
|
$fx_providers = [
|
|
'open_er_api' => 'open.er-api.com (free)',
|
|
'iran_market' => __('accounting::lang.iran_market_provider'),
|
|
'exchangerate_api_v4' => 'ExchangeRate-API v4 (free)',
|
|
'exchangerate_api_v6' => 'ExchangeRate-API v6 (API key)',
|
|
];
|
|
|
|
return view('accounting::holding.exchange_rates', compact('rates', 'base_currency_code', 'accounting_settings', 'fx_providers'));
|
|
}
|
|
|
|
public function storeExchangeRate(Request $request, ExchangeRateService $exchangeRateService)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
|
|
$request->validate([
|
|
'from_currency' => 'required|string|max:10',
|
|
'to_currency' => 'required|string|max:10',
|
|
'rate' => 'required|numeric|min:0.000001',
|
|
'rate_date' => 'required|date',
|
|
]);
|
|
|
|
$exchangeRateService->upsertRate(
|
|
$business_id,
|
|
$request->from_currency,
|
|
$request->to_currency,
|
|
(float) $request->rate,
|
|
$request->rate_date,
|
|
'manual'
|
|
);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]);
|
|
}
|
|
|
|
public function syncExchangeRates(Request $request, ExchangeRateService $exchangeRateService)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
|
|
$result = $exchangeRateService->syncForBusiness(
|
|
$business_id,
|
|
$request->input('rate_date', now()->toDateString()),
|
|
true
|
|
);
|
|
|
|
return redirect()->back()->with('status', [
|
|
'success' => 1,
|
|
'msg' => __('accounting::lang.exchange_rates_synced', [
|
|
'upserted' => $result['upserted'],
|
|
'entities' => $result['entities_updated'],
|
|
'api' => $result['api_fetched'] ?? 0,
|
|
]),
|
|
]);
|
|
}
|
|
|
|
public function saveExchangeRateSettings(Request $request, AccountingUtil $accountingUtil)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
|
|
$existing = $accountingUtil->getAccountingSettings($business_id);
|
|
$merged = array_merge($existing, [
|
|
'fx_api_enabled' => $request->has('fx_api_enabled') ? 1 : 0,
|
|
'fx_api_provider' => $request->input('fx_api_provider', config('accounting.fx_api.default_provider', 'open_er_api')),
|
|
'fx_api_key' => $request->input('fx_api_key', ''),
|
|
]);
|
|
|
|
Business::where('id', $business_id)->update(['accounting_settings' => json_encode($merged)]);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.updated_success')]);
|
|
}
|
|
|
|
public function runConsolidation(Request $request, ConsolidationService $consolidationService)
|
|
{
|
|
$this->authorizeAccounting('accounting.view_reports');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
|
|
$request->validate([
|
|
'period_start' => 'required|date',
|
|
'period_end' => 'required|date|after_or_equal:period_start',
|
|
'consolidation_currency_code' => 'nullable|string|max:10',
|
|
'entity_rate' => 'nullable|array',
|
|
]);
|
|
|
|
$entities = HoldingEntity::where('business_id', $business_id)
|
|
->where('is_active', true)
|
|
->orderBy('name')
|
|
->get(['id', 'name', 'meta']);
|
|
$defaults = $this->buildConsolidationDefaults(
|
|
$business_id,
|
|
$entities,
|
|
(string) $request->input('consolidation_currency_code', 'IRR')
|
|
);
|
|
|
|
$rawRates = (array) $request->input('entity_rate', $defaults['entity_rates']);
|
|
$entityRates = [];
|
|
foreach ($rawRates as $entityId => $rate) {
|
|
$entityRates[(int) $entityId] = max(1.0E-10, (float) $rate);
|
|
}
|
|
|
|
$rawOfficial = (array) $request->input('entity_official_rate', $defaults['official_entity_rates']);
|
|
$officialRates = [];
|
|
foreach ($rawOfficial as $entityId => $rate) {
|
|
$officialRates[(int) $entityId] = max(1.0E-10, (float) $rate);
|
|
}
|
|
|
|
$consolidationService->run(
|
|
$business_id,
|
|
$request->period_start,
|
|
$request->period_end,
|
|
$user_id,
|
|
$request->input('parent_entity_id'),
|
|
(string) $request->input('consolidation_currency_code', $defaults['target_currency_code']),
|
|
$entityRates,
|
|
(string) $request->input('rate_mode', 'market'),
|
|
$officialRates
|
|
);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.consolidation_run_created')]);
|
|
}
|
|
|
|
public function finalizeConsolidation($id, ConsolidationService $consolidationService)
|
|
{
|
|
$this->authorizeAccounting('accounting.view_reports');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$run = AccountingConsolidationRun::where('business_id', $business_id)->findOrFail($id);
|
|
$consolidationService->finalize($run);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.consolidation_finalized')]);
|
|
}
|
|
|
|
// --- Payment account GL link ---
|
|
public function linkPaymentAccount(Request $request, FinancialPostingService $postingService)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$request->validate([
|
|
'payment_account_id' => 'required|integer',
|
|
'accounting_account_id' => 'required|integer',
|
|
]);
|
|
|
|
$postingService->linkPaymentAccountToGl(
|
|
(int) $business_id,
|
|
(int) $request->payment_account_id,
|
|
(int) $request->accounting_account_id
|
|
);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.payment_account_linked')]);
|
|
}
|
|
|
|
public function autoLinkPaymentAccounts(PaymentAccountGlLinkService $linkService)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = (int) request()->session()->get('user.business_id');
|
|
$linked = $linkService->linkByNameMatch($business_id);
|
|
|
|
return redirect()->back()->with('status', [
|
|
'success' => 1,
|
|
'msg' => __('accounting::lang.payment_accounts_auto_linked', ['count' => $linked]),
|
|
]);
|
|
}
|
|
|
|
public function postFinancialContract(Request $request, FinancialPostingService $postingService)
|
|
{
|
|
$this->authorizeAccounting('accounting.add_journal');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
|
|
$payload = $request->validate([
|
|
'source_type' => 'required|string|max:100',
|
|
'source_id' => 'required',
|
|
'note' => 'nullable|string|max:5000',
|
|
'operation_date' => 'nullable|date',
|
|
'mapping_type' => 'nullable|string|max:100',
|
|
'ref_no' => 'nullable|string|max:191',
|
|
'cost_center_id' => 'nullable|integer',
|
|
'project_id' => 'nullable|integer',
|
|
'holding_entity_id' => 'nullable|integer',
|
|
'allow_duplicate' => 'nullable|boolean',
|
|
'lines' => 'required|array|min:2',
|
|
'lines.*.accounting_account_id' => 'required|integer',
|
|
'lines.*.amount' => 'required|numeric|min:0.000001',
|
|
'lines.*.type' => 'required|in:debit,credit',
|
|
'lines.*.sub_type' => 'nullable|string|max:100',
|
|
'lines.*.note' => 'nullable|string|max:500',
|
|
'lines.*.cost_center_id' => 'nullable|integer',
|
|
'lines.*.project_id' => 'nullable|integer',
|
|
'lines.*.holding_entity_id' => 'nullable|integer',
|
|
]);
|
|
|
|
$postingRequest = new FinancialPostingRequest(
|
|
businessId: (int) $business_id,
|
|
userId: (int) $user_id,
|
|
sourceType: (string) $payload['source_type'],
|
|
sourceId: $payload['source_id'],
|
|
note: (string) ($payload['note'] ?? ''),
|
|
lines: $payload['lines'],
|
|
operationDate: $payload['operation_date'] ?? null,
|
|
costCenterId: isset($payload['cost_center_id']) ? (int) $payload['cost_center_id'] : null,
|
|
projectId: isset($payload['project_id']) ? (int) $payload['project_id'] : null,
|
|
holdingEntityId: isset($payload['holding_entity_id']) ? (int) $payload['holding_entity_id'] : null,
|
|
mappingType: (string) ($payload['mapping_type'] ?? 'journal_entry'),
|
|
refNo: $payload['ref_no'] ?? null,
|
|
);
|
|
|
|
$mapping = $postingService->post($postingRequest, (bool) ($payload['allow_duplicate'] ?? false));
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'posting_key' => $postingRequest->postingKey(),
|
|
'mapping_id' => $mapping?->id,
|
|
'message' => __('accounting::lang.financial_contract_posted'),
|
|
]);
|
|
}
|
|
|
|
public function mappingGaps(LocationDefaultMappingService $mappingService)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = (int) request()->session()->get('user.business_id');
|
|
$report = $mappingService->assess($business_id);
|
|
|
|
if (request()->expectsJson() || request()->ajax()) {
|
|
return response()->json($report);
|
|
}
|
|
|
|
return view('accounting::holding.mapping_gaps', compact('report'));
|
|
}
|
|
|
|
public function syncLocationMappings(Request $request, LocationDefaultMappingService $mappingService)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = (int) request()->session()->get('user.business_id');
|
|
$overwrite = (bool) $request->input('overwrite', false);
|
|
|
|
$result = $mappingService->sync($business_id, $overwrite);
|
|
|
|
if ($request->expectsJson() || $request->ajax()) {
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => __('accounting::lang.location_mapping_synced', [
|
|
'locations' => $result['updated_locations'],
|
|
'fields' => $result['filled_fields'],
|
|
'payment_links' => $result['payment_accounts_linked'],
|
|
]),
|
|
'result' => $result,
|
|
]);
|
|
}
|
|
|
|
return redirect()->back()->with('status', [
|
|
'success' => 1,
|
|
'msg' => __('accounting::lang.location_mapping_synced', [
|
|
'locations' => $result['updated_locations'],
|
|
'fields' => $result['filled_fields'],
|
|
'payment_links' => $result['payment_accounts_linked'],
|
|
]),
|
|
]);
|
|
}
|
|
|
|
public function analyticalDimensions()
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$dimensions = AccountingAnalyticalDimension::where('business_id', $business_id)->orderBy('dimension_type')->get();
|
|
|
|
return view('accounting::holding.analytical_dimensions', compact('dimensions'));
|
|
}
|
|
|
|
public function storeAnalyticalDimension(Request $request)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
|
|
$request->validate([
|
|
'dimension_type' => 'required|in:cost_center,project,department',
|
|
'code' => 'required|string|max:50',
|
|
'name' => 'required|string|max:191',
|
|
]);
|
|
|
|
AccountingAnalyticalDimension::create([
|
|
'business_id' => $business_id,
|
|
'dimension_type' => $request->dimension_type,
|
|
'code' => $request->code,
|
|
'name' => $request->name,
|
|
'holding_entity_id' => $request->holding_entity_id,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]);
|
|
}
|
|
|
|
public function journalApprovals()
|
|
{
|
|
$this->authorizeAccounting('accounting.view_journal');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$journals = AccountingAccTransMapping::where('business_id', $business_id)
|
|
->where('type', 'journal_entry')
|
|
->where('approval_status', 'pending')
|
|
->orderByDesc('id')
|
|
->paginate(20);
|
|
|
|
return view('accounting::holding.journal_approvals', compact('journals'));
|
|
}
|
|
|
|
public function approveJournal($id, JournalApprovalService $approvalService)
|
|
{
|
|
$this->authorizeAccounting('accounting.edit_journal');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
$journal = AccountingAccTransMapping::where('business_id', $business_id)->findOrFail($id);
|
|
$approvalService->approve($journal, $user_id);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.journal_approved')]);
|
|
}
|
|
|
|
public function rejectJournal($id, JournalApprovalService $approvalService)
|
|
{
|
|
$this->authorizeAccounting('accounting.edit_journal');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
$journal = AccountingAccTransMapping::where('business_id', $business_id)->findOrFail($id);
|
|
$approvalService->reject($journal, $user_id);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.journal_rejected')]);
|
|
}
|
|
|
|
public function openingBalance(OpeningBalanceService $openingBalanceService)
|
|
{
|
|
$this->authorizeAccounting('accounting.add_journal');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$accounts = AccountingAccount::forDropdown($business_id, false);
|
|
$inactiveAccounts = $openingBalanceService->accountsWithoutActivity($business_id);
|
|
|
|
return view('accounting::holding.opening_balance', compact('accounts', 'inactiveAccounts'));
|
|
}
|
|
|
|
public function storeOpeningBalance(Request $request, OpeningBalanceService $openingBalanceService)
|
|
{
|
|
$this->authorizeAccounting('accounting.add_journal');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
|
|
$lines = [];
|
|
foreach ($request->input('account_id', []) as $index => $accountId) {
|
|
$debit = $request->input('debit.'.$index);
|
|
$credit = $request->input('credit.'.$index);
|
|
if (! empty($debit)) {
|
|
$lines[] = ['accounting_account_id' => $accountId, 'amount' => $debit, 'type' => 'debit'];
|
|
}
|
|
if (! empty($credit)) {
|
|
$lines[] = ['accounting_account_id' => $accountId, 'amount' => $credit, 'type' => 'credit'];
|
|
}
|
|
}
|
|
|
|
$openingBalanceService->post(
|
|
$business_id,
|
|
$user_id,
|
|
$request->input('operation_date', now()->toDateString()),
|
|
$lines,
|
|
$request->input('note')
|
|
);
|
|
|
|
return redirect()->route('accounting.openingBalance')->with('status', ['success' => 1, 'msg' => __('accounting::lang.opening_balance_posted')]);
|
|
}
|
|
|
|
// --- Bank loans ---
|
|
public function bankLoans()
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$loans = AccountingBankLoan::where('business_id', $business_id)
|
|
->with('payments')
|
|
->orderByDesc('id')
|
|
->paginate(20);
|
|
$accounts = AccountingAccount::forDropdown($business_id, false);
|
|
|
|
return view('accounting::holding.bank_loans', compact('loans', 'accounts'));
|
|
}
|
|
|
|
public function storeBankLoan(Request $request, BankLoanService $bankLoanService)
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
|
|
$request->validate([
|
|
'loan_number' => 'required|string|max:100',
|
|
'lender_name' => 'required|string|max:191',
|
|
'principal_amount' => 'required|numeric|min:0.01',
|
|
'disbursement_date' => 'required|date',
|
|
]);
|
|
|
|
$bankLoanService->disburse($request->only([
|
|
'loan_number', 'lender_name', 'principal_amount', 'interest_rate', 'term_months',
|
|
'disbursement_date', 'maturity_date', 'loan_account_id', 'bank_account_id', 'note',
|
|
]), $business_id, $user_id);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]);
|
|
}
|
|
|
|
public function payBankLoan($id, Request $request, BankLoanService $bankLoanService)
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
$loan = AccountingBankLoan::where('business_id', $business_id)->findOrFail($id);
|
|
|
|
$request->validate([
|
|
'payment_date' => 'required|date',
|
|
'principal_amount' => 'nullable|numeric|min:0',
|
|
'interest_amount' => 'nullable|numeric|min:0',
|
|
]);
|
|
|
|
$bankLoanService->recordPayment($loan, $request->only([
|
|
'payment_date', 'principal_amount', 'interest_amount', 'bank_account_id', 'interest_account_id', 'note',
|
|
]), $user_id);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.loan_payment_recorded')]);
|
|
}
|
|
|
|
// --- Insurance ---
|
|
public function insurancePremiums()
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$premiums = AccountingInsurancePremium::where('business_id', $business_id)
|
|
->orderByDesc('id')
|
|
->paginate(20);
|
|
$accounts = AccountingAccount::forDropdown($business_id, false);
|
|
|
|
return view('accounting::holding.insurance', compact('premiums', 'accounts'));
|
|
}
|
|
|
|
public function storeInsurancePremium(Request $request, InsurancePremiumService $insuranceService)
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
|
|
$request->validate([
|
|
'provider_name' => 'required|string|max:191',
|
|
'amount' => 'required|numeric|min:0.01',
|
|
]);
|
|
|
|
$insuranceService->register($request->only([
|
|
'policy_number', 'provider_name', 'premium_type', 'amount', 'due_date', 'note',
|
|
]), $business_id, $user_id);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]);
|
|
}
|
|
|
|
public function payInsurancePremium($id, Request $request, InsurancePremiumService $insuranceService)
|
|
{
|
|
$this->authorizeAccounting('accounting.map_transactions');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$user_id = request()->session()->get('user.id');
|
|
$premium = AccountingInsurancePremium::where('business_id', $business_id)->findOrFail($id);
|
|
|
|
$request->validate([
|
|
'paid_date' => 'required|date',
|
|
'expense_account_id' => 'required|integer',
|
|
'bank_account_id' => 'required|integer',
|
|
]);
|
|
|
|
$insuranceService->markPaid($premium, $request->only([
|
|
'paid_date', 'expense_account_id', 'bank_account_id',
|
|
]), $user_id);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.insurance_premium_paid')]);
|
|
}
|
|
|
|
// --- Payroll tax table ---
|
|
public function payrollTaxTable()
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$brackets = AccountingPayrollTaxBracket::where('business_id', $business_id)
|
|
->orderBy('sort_order')
|
|
->orderBy('min_amount')
|
|
->get();
|
|
|
|
return view('accounting::holding.payroll_tax_table', compact('brackets'));
|
|
}
|
|
|
|
public function storePayrollTaxBracket(Request $request, PayrollTaxTableService $taxTableService)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
|
|
$request->validate([
|
|
'name' => 'required|string|max:191',
|
|
'min_amount' => 'required|numeric|min:0',
|
|
'rate_percent' => 'required|numeric|min:0',
|
|
]);
|
|
|
|
$taxTableService->store($request->only([
|
|
'name', 'min_amount', 'max_amount', 'rate_percent', 'fixed_deduction', 'sort_order', 'is_active',
|
|
]), $business_id);
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]);
|
|
}
|
|
|
|
public function updatePayrollTaxBracket($id, Request $request, PayrollTaxTableService $taxTableService)
|
|
{
|
|
$this->authorizeAccounting('accounting.manage_accounts');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$bracket = AccountingPayrollTaxBracket::where('business_id', $business_id)->findOrFail($id);
|
|
|
|
$request->validate([
|
|
'name' => 'required|string|max:191',
|
|
'min_amount' => 'required|numeric|min:0',
|
|
'rate_percent' => 'required|numeric|min:0',
|
|
]);
|
|
|
|
$taxTableService->update($bracket, $request->only([
|
|
'name', 'min_amount', 'max_amount', 'rate_percent', 'fixed_deduction', 'sort_order', 'is_active',
|
|
]));
|
|
|
|
return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.updated_success')]);
|
|
}
|
|
|
|
public function deferredDocuments(DeferredDocumentsService $deferredService)
|
|
{
|
|
$this->authorizeAccounting('accounting.view_reports');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
$summary = $deferredService->summary($business_id);
|
|
$items = $deferredService->items($business_id);
|
|
|
|
return view('accounting::holding.deferred_documents', compact('summary', 'items'));
|
|
}
|
|
|
|
public function chequeReport()
|
|
{
|
|
$this->authorizeAccounting('accounting.view_reports');
|
|
$business_id = request()->session()->get('user.business_id');
|
|
|
|
$query = AccountingCheque::where('business_id', $business_id);
|
|
if (request()->filled('direction')) {
|
|
$query->where('direction', request('direction'));
|
|
}
|
|
if (request()->filled('status')) {
|
|
$query->where('status', request('status'));
|
|
}
|
|
|
|
$all = AccountingCheque::where('business_id', $business_id)->get();
|
|
$summary = [
|
|
'received_pending' => [
|
|
'label' => __('accounting::lang.received_pending_cheques'),
|
|
'count' => $all->where('direction', 'received')->whereIn('status', ['pending', 'deposited'])->count(),
|
|
'amount' => $all->where('direction', 'received')->whereIn('status', ['pending', 'deposited'])->sum('amount'),
|
|
],
|
|
'paid_pending' => [
|
|
'label' => __('accounting::lang.paid_pending_cheques'),
|
|
'count' => $all->where('direction', 'paid')->whereIn('status', ['pending', 'deposited'])->count(),
|
|
'amount' => $all->where('direction', 'paid')->whereIn('status', ['pending', 'deposited'])->sum('amount'),
|
|
],
|
|
'cleared' => [
|
|
'label' => __('accounting::lang.cleared_cheques'),
|
|
'count' => $all->where('status', 'cleared')->count(),
|
|
'amount' => $all->where('status', 'cleared')->sum('amount'),
|
|
],
|
|
'bounced' => [
|
|
'label' => __('accounting::lang.bounced_cheques'),
|
|
'count' => $all->where('status', 'bounced')->count(),
|
|
'amount' => $all->where('status', 'bounced')->sum('amount'),
|
|
],
|
|
];
|
|
|
|
$cheques = $query->orderByDesc('due_date')->paginate(50);
|
|
|
|
return view('accounting::holding.cheque_report', compact('summary', 'cheques'));
|
|
}
|
|
|
|
private function buildConsolidationDefaults(int $businessId, $entities, ?string $targetCurrencyCode): array
|
|
{
|
|
$exchangeRateService = app(ExchangeRateService::class);
|
|
$business = Business::with('currency')->find($businessId);
|
|
$baseCurrencyCode = (string) optional($business->currency)->code ?: 'IRR';
|
|
$target = strtoupper($targetCurrencyCode ?: $baseCurrencyCode);
|
|
$entityRates = [];
|
|
$officialEntityRates = [];
|
|
|
|
foreach ($entities as $entity) {
|
|
$entityCurrency = strtoupper((string) data_get($entity->meta, 'currency_code', $baseCurrencyCode));
|
|
$entityRates[(int) $entity->id] = $exchangeRateService->resolveEntityRate(
|
|
$businessId,
|
|
$entityCurrency,
|
|
$target,
|
|
(array) $entity->meta
|
|
);
|
|
$officialEntityRates[(int) $entity->id] = $exchangeRateService->getRateByType(
|
|
$businessId,
|
|
$entityCurrency,
|
|
$target,
|
|
'official'
|
|
) ?? $entityRates[(int) $entity->id];
|
|
}
|
|
|
|
return [
|
|
'base_currency_code' => $baseCurrencyCode,
|
|
'target_currency_code' => $target,
|
|
'entity_rates' => $entityRates,
|
|
'official_entity_rates' => $officialEntityRates,
|
|
];
|
|
}
|
|
}
|