feat(client): add financial reports and ledger management
- Add trial balance and general ledger pages to client resource with interactive tables - Implement sales and expenses relation managers for client-specific transactions - Enhance transaction handling with proper tax and withholding calculations - Add date casting to Transaction model and define client relationships - Configure super admin role bypass in AppServiceProvider - Update Filament components and fix JavaScript formatting issues
This commit is contained in:
@@ -25,6 +25,7 @@ class CreateLedgerAction extends BaseAction
|
||||
ledger_id: $ledger->id,
|
||||
account_id: $ledger->account_id,
|
||||
branch_id: $ledger->branch_id,
|
||||
type: $payload->type ?? 'debit',
|
||||
);
|
||||
|
||||
return $next($payload);
|
||||
|
||||
@@ -30,6 +30,8 @@ class CreateTransactionAction extends BaseAction
|
||||
public function transactionAccountLedger($payload): void
|
||||
{
|
||||
$branch = $payload->transaction->branch;
|
||||
$isExpense = $payload->transactionable instanceof \App\Models\Expense;
|
||||
$type = $isExpense ? 'debit' : 'credit';
|
||||
|
||||
if ($branch->isClientVatable) {
|
||||
//create transaction account ledger
|
||||
@@ -38,10 +40,11 @@ class CreateTransactionAction extends BaseAction
|
||||
amount: $payload->transaction->net_amount ?? 0.00,
|
||||
transaction: $payload->transaction,
|
||||
account: $payload->transaction->account,
|
||||
type: $type,
|
||||
);
|
||||
$this->ledgerPipe($ledgerPayload);
|
||||
|
||||
$this->inputTaxAccountLedger($payload);
|
||||
$this->taxAccountLedger($payload, $isExpense);
|
||||
} else {
|
||||
//create transaction account ledger
|
||||
$ledgerPayload = new CreateLedgerDTO(
|
||||
@@ -49,6 +52,7 @@ class CreateTransactionAction extends BaseAction
|
||||
amount: $payload->transaction->gross_amount ?? 0.00,
|
||||
transaction: $payload->transaction,
|
||||
account: $payload->transaction->account,
|
||||
type: $type,
|
||||
);
|
||||
$this->ledgerPipe($ledgerPayload);
|
||||
}
|
||||
@@ -63,50 +67,69 @@ class CreateTransactionAction extends BaseAction
|
||||
])->thenReturn();
|
||||
}
|
||||
|
||||
public function inputTaxAccountLedger($payload): void
|
||||
public function taxAccountLedger($payload, bool $isExpense): void
|
||||
{
|
||||
$inputTax = Account::query()->where('account', 'Input Tax')->whereHas('balances', function ($balance) use ($payload) {
|
||||
$accountName = $isExpense ? 'Input Tax' : 'Output Tax';
|
||||
$type = $isExpense ? 'debit' : 'credit';
|
||||
|
||||
$taxAccount = Account::query()->where('account', $accountName)->whereHas('balances', function ($balance) use ($payload) {
|
||||
return $balance->where('branch_id', $payload->transactionable->branch_id);
|
||||
})->first();
|
||||
|
||||
$ledgerPayload = new CreateLedgerDTO(
|
||||
branch_id: $payload->transactionable->branch_id,
|
||||
amount: $payload->transactionable->input_tax ?? 0.00,
|
||||
transaction: $payload->transaction,
|
||||
account: $inputTax,
|
||||
);
|
||||
$this->ledgerPipe($ledgerPayload);
|
||||
if ($taxAccount) {
|
||||
$ledgerPayload = new CreateLedgerDTO(
|
||||
branch_id: $payload->transactionable->branch_id,
|
||||
amount: $payload->transactionable->input_tax ?? 0.00, // Assuming input_tax holds the tax amount for both? Or output_tax?
|
||||
transaction: $payload->transaction,
|
||||
account: $taxAccount,
|
||||
type: $type,
|
||||
);
|
||||
$this->ledgerPipe($ledgerPayload);
|
||||
}
|
||||
}
|
||||
|
||||
public function withHoldingAccountLedger($payload): void
|
||||
{
|
||||
$withholdingAccount = Account::query()->where('account', 'Payable Withholding Tax')->whereHas('balances', function ($balance) use ($payload) {
|
||||
$isExpense = $payload->transactionable instanceof \App\Models\Expense;
|
||||
$accountName = $isExpense ? 'Payable Withholding Tax' : 'Creditable Withholding Tax';
|
||||
$type = $isExpense ? 'credit' : 'debit';
|
||||
|
||||
$withholdingAccount = Account::query()->where('account', $accountName)->whereHas('balances', function ($balance) use ($payload) {
|
||||
return $balance->where('branch_id', $payload->transactionable->branch_id);
|
||||
})->first();
|
||||
|
||||
$ledgerPayload = new CreateLedgerDTO(
|
||||
branch_id: $payload->transactionable->branch_id,
|
||||
amount: $payload->transaction->payable_withholding_tax ?? 0.00,
|
||||
transaction: $payload->transaction,
|
||||
account: $withholdingAccount,
|
||||
);
|
||||
$this->ledgerPipe($ledgerPayload);
|
||||
if ($withholdingAccount) {
|
||||
$ledgerPayload = new CreateLedgerDTO(
|
||||
branch_id: $payload->transactionable->branch_id,
|
||||
amount: $payload->transaction->payable_withholding_tax ?? 0.00,
|
||||
transaction: $payload->transaction,
|
||||
account: $withholdingAccount,
|
||||
type: $type,
|
||||
);
|
||||
$this->ledgerPipe($ledgerPayload);
|
||||
}
|
||||
}
|
||||
|
||||
public function cashAccountLedger($payload): void
|
||||
{
|
||||
$isExpense = $payload->transactionable instanceof \App\Models\Expense;
|
||||
$type = $isExpense ? 'credit' : 'debit';
|
||||
$wht = $isExpense ? ($payload->transaction->payable_withholding_tax ?? 0) : ($payload->transaction->creditable_withholding_tax ?? 0);
|
||||
$amount = ($payload->transaction->gross_amount ?? 0) - $wht;
|
||||
|
||||
$cashAccount = Account::query()->where('account', 'Cash')->whereHas('balances', function ($balance) use ($payload) {
|
||||
return $balance->where('branch_id', $payload->transactionable->branch_id);
|
||||
})->first();
|
||||
|
||||
$amount = $payload->transaction->gross_amount - $payload->transaction->creditable_withholding_tax;
|
||||
|
||||
$ledgerPayload = new CreateLedgerDTO(
|
||||
branch_id: $payload->transactionable->branch_id,
|
||||
amount: $amount ?? 0.00,
|
||||
transaction: $payload->transaction,
|
||||
account: $cashAccount,
|
||||
);
|
||||
$this->ledgerPipe($ledgerPayload);
|
||||
if ($cashAccount) {
|
||||
$ledgerPayload = new CreateLedgerDTO(
|
||||
branch_id: $payload->transactionable->branch_id,
|
||||
amount: $amount,
|
||||
transaction: $payload->transaction,
|
||||
account: $cashAccount,
|
||||
type: $type,
|
||||
);
|
||||
$this->ledgerPipe($ledgerPayload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,16 +15,27 @@ class CreateBalanceDTO extends Data
|
||||
public ?int $id = null,
|
||||
public ?int $ledger_id = null,
|
||||
public ?int $account_id = null,
|
||||
public ?int $branch_id = null
|
||||
public ?int $branch_id = null,
|
||||
public string $type = 'debit'
|
||||
) {
|
||||
$account = Account::query()->where('id', $this->account_id)->first();
|
||||
$account = Account::with('accountType')->where('id', $this->account_id)->first();
|
||||
|
||||
$currentBalance = $account ? $account->current_balance : 0;
|
||||
$normalBalance = strtolower($account->accountType->normal_balance ?? 'debit');
|
||||
$transactionType = strtolower($this->type);
|
||||
|
||||
if ($account->normal_balance == 'credit') {
|
||||
$this->balance = $currentBalance - $this->amount;
|
||||
} else {
|
||||
$this->balance = $currentBalance + $this->amount;
|
||||
if ($transactionType === 'debit') {
|
||||
if ($normalBalance === 'debit') {
|
||||
$this->balance = $currentBalance + $this->amount;
|
||||
} else {
|
||||
$this->balance = $currentBalance - $this->amount;
|
||||
}
|
||||
} else { // credit
|
||||
if ($normalBalance === 'credit') {
|
||||
$this->balance = $currentBalance + $this->amount;
|
||||
} else {
|
||||
$this->balance = $currentBalance - $this->amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ class CreateLedgerDTO extends Data
|
||||
public array $data;
|
||||
|
||||
#[Computed]
|
||||
public int $transaction_id;
|
||||
public ?int $transaction_id = null;
|
||||
|
||||
#[Computed]
|
||||
public ?int $journal_id = null;
|
||||
|
||||
#[Computed]
|
||||
public int $account_id;
|
||||
@@ -35,16 +38,22 @@ class CreateLedgerDTO extends Data
|
||||
public float $amount,
|
||||
public ?Model $ledger = null,
|
||||
public ?Model $transaction = null,
|
||||
public ?Model $journal = null,
|
||||
public ?Model $account = null,
|
||||
public ?CreateBalanceDTO $balanceDTO = null,
|
||||
public ?string $type = null, // 'debit' or 'credit'
|
||||
) {
|
||||
$this->transaction_id = $this->transaction->id;
|
||||
$this->transaction_id = $this->transaction?->id;
|
||||
$this->journal_id = $this->journal?->id;
|
||||
$this->account_id = $this->account->id;
|
||||
$this->client_id = $this->transaction->branch->client_id;
|
||||
$this->credit_amount = $this->transaction->account_type == 'credit' ? $this->amount : 0.00;
|
||||
$this->debit_amount = $this->transaction->account_type == 'debit' ? $this->amount : 0.00;
|
||||
$this->description = $this->transaction->description;
|
||||
$this->client_id = $this->transaction?->branch->client_id ?? $this->journal?->client_id;
|
||||
|
||||
$accountType = $this->type ? strtolower($this->type) : strtolower($this->transaction?->account_type);
|
||||
|
||||
$this->data = Arr::except($this->toArray(), ['transaction', 'ledger', 'account', 'amount']);
|
||||
$this->credit_amount = $accountType == 'credit' ? $this->amount : 0.00;
|
||||
$this->debit_amount = $accountType == 'debit' ? $this->amount : 0.00;
|
||||
$this->description = $this->transaction?->description ?? $this->journal?->description;
|
||||
|
||||
$this->data = Arr::except($this->toArray(), ['transaction', 'journal', 'ledger', 'account', 'amount', 'type']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class GeneralLedger extends Page
|
||||
{
|
||||
protected static ?string $navigationIcon = 'heroicon-o-document-text';
|
||||
|
||||
protected static string $view = 'filament.pages.general-ledger';
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class TrialBalance extends Page
|
||||
{
|
||||
protected static ?string $navigationIcon = 'heroicon-o-document-text';
|
||||
|
||||
protected static string $view = 'filament.pages.trial-balance';
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,13 @@ use App\DataObjects\CreateBranchDTO;
|
||||
use App\Filament\Resources\ClientResource\Pages\EditClient;
|
||||
use App\Filament\Resources\ClientResource\Pages\ListClients;
|
||||
use App\Filament\Resources\ClientResource\Pages\ViewClient;
|
||||
use App\Filament\Resources\ClientResource\Pages\GeneralLedger;
|
||||
use App\Filament\Resources\ClientResource\Pages\TrialBalance;
|
||||
use App\Filament\Resources\ClientResource\RelationManagers\AccountsRelationManager;
|
||||
use App\Filament\Resources\ClientResource\RelationManagers\BranchesRelationManager;
|
||||
use App\Filament\Resources\ClientResource\RelationManagers\ExpensesRelationManager;
|
||||
use App\Filament\Resources\ClientResource\RelationManagers\JournalsRelationManager;
|
||||
use App\Filament\Resources\ClientResource\RelationManagers\SalesRelationManager;
|
||||
use App\Filament\Resources\ClientResource\RelationManagers\TransmittalsRelationManager;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Client;
|
||||
@@ -19,6 +24,8 @@ use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Filament\Pages\SubNavigationPosition;
|
||||
use Filament\Resources\Pages\Page;
|
||||
|
||||
class ClientResource extends Resource
|
||||
{
|
||||
@@ -26,11 +33,25 @@ class ClientResource extends Resource
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-user';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'company';
|
||||
|
||||
protected static SubNavigationPosition $subNavigationPosition = SubNavigationPosition::Top;
|
||||
|
||||
public static function authorizeView(Model $record): void
|
||||
{
|
||||
parent::authorizeView($record);
|
||||
}
|
||||
|
||||
public static function getRecordSubNavigation(Page $page): array
|
||||
{
|
||||
return $page->generateNavigationItems([
|
||||
ViewClient::class,
|
||||
EditClient::class,
|
||||
GeneralLedger::class,
|
||||
TrialBalance::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
@@ -85,6 +106,9 @@ class ClientResource extends Resource
|
||||
AccountsRelationManager::class,
|
||||
BranchesRelationManager::class,
|
||||
TransmittalsRelationManager::class,
|
||||
SalesRelationManager::class,
|
||||
ExpensesRelationManager::class,
|
||||
JournalsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -94,6 +118,8 @@ class ClientResource extends Resource
|
||||
'view' => ViewClient::route('/{record}'),
|
||||
'edit' => EditClient::route('/{record}/edit'),
|
||||
'index' => ListClients::route('/'),
|
||||
'general-ledger' => GeneralLedger::route('/{record}/general-ledger'),
|
||||
'trial-balance' => TrialBalance::route('/{record}/trial-balance'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
142
app/Filament/Resources/ClientResource/Pages/GeneralLedger.php
Normal file
142
app/Filament/Resources/ClientResource/Pages/GeneralLedger.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ClientResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ClientResource;
|
||||
use App\Models\Ledger;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Filament\Resources\Pages\Concerns\InteractsWithRecord;
|
||||
use pxlrbt\FilamentExcel\Actions\Tables\ExportAction;
|
||||
use pxlrbt\FilamentExcel\Exports\ExcelExport;
|
||||
use pxlrbt\FilamentExcel\Columns\Column;
|
||||
|
||||
class GeneralLedger extends Page implements HasTable
|
||||
{
|
||||
use InteractsWithTable;
|
||||
use InteractsWithRecord;
|
||||
|
||||
protected static string $resource = ClientResource::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-document-text';
|
||||
|
||||
protected static string $view = 'filament.resources.client-resource.pages.general-ledger';
|
||||
|
||||
public function mount(int | string $record): void
|
||||
{
|
||||
$this->record = $this->resolveRecord($record);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query(
|
||||
Ledger::query()
|
||||
->where('client_id', $this->getRecord()->id)
|
||||
->with(['account', 'transaction', 'journal'])
|
||||
)
|
||||
->columns([
|
||||
TextColumn::make('date')
|
||||
->label('Date')
|
||||
->state(function (Ledger $record) {
|
||||
return $record->transaction?->happened_on?->format('Y-m-d')
|
||||
?? $record->journal?->happened_on?->format('Y-m-d')
|
||||
?? $record->created_at->format('Y-m-d');
|
||||
})
|
||||
->sortable(),
|
||||
TextColumn::make('account.account')
|
||||
->label('Account')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('description')
|
||||
->limit(50)
|
||||
->tooltip(function (TextColumn $column): ?string {
|
||||
$state = $column->getState();
|
||||
if (strlen($state) <= $column->getCharacterLimit()) {
|
||||
return null;
|
||||
}
|
||||
return $state;
|
||||
}),
|
||||
TextColumn::make('debit_amount')
|
||||
->label('Debit')
|
||||
->money('PHP')
|
||||
->sortable(),
|
||||
TextColumn::make('credit_amount')
|
||||
->label('Credit')
|
||||
->money('PHP')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('account')
|
||||
->relationship('account', 'account', fn (Builder $query) => $query->where('client_id', $this->getRecord()->id))
|
||||
->searchable()
|
||||
->preload(),
|
||||
Filter::make('date_range')
|
||||
->form([
|
||||
DatePicker::make('from'),
|
||||
DatePicker::make('to'),
|
||||
])
|
||||
->query(function (Builder $query, array $data): Builder {
|
||||
return $query
|
||||
->when(
|
||||
$data['from'],
|
||||
fn (Builder $query, $date): Builder => $query->where(function ($q) use ($date) {
|
||||
$q->whereHas('transaction', fn ($q) => $q->whereDate('happened_on', '>=', $date))
|
||||
->orWhereHas('journal', fn ($q) => $q->whereDate('happened_on', '>=', $date))
|
||||
->orWhere(fn($q) => $q->whereDoesntHave('transaction')->whereDoesntHave('journal')->whereDate('created_at', '>=', $date));
|
||||
})
|
||||
)
|
||||
->when(
|
||||
$data['to'],
|
||||
fn (Builder $query, $date): Builder => $query->where(function ($q) use ($date) {
|
||||
$q->whereHas('transaction', fn ($q) => $q->whereDate('happened_on', '<=', $date))
|
||||
->orWhereHas('journal', fn ($q) => $q->whereDate('happened_on', '<=', $date))
|
||||
->orWhere(fn($q) => $q->whereDoesntHave('transaction')->whereDoesntHave('journal')->whereDate('created_at', '<=', $date));
|
||||
})
|
||||
);
|
||||
})
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->groups([
|
||||
'account.account',
|
||||
])
|
||||
->headerActions([
|
||||
ExportAction::make()
|
||||
->label('Export General Ledger')
|
||||
->exports([
|
||||
ExcelExport::make()
|
||||
->fromTable()
|
||||
->withFilename(fn () => $this->getRecord()->name . ' - General Ledger - ' . date('Y-m-d'))
|
||||
->withColumns([
|
||||
Column::make('date')
|
||||
->heading('Date')
|
||||
->formatStateUsing(fn ($record) => $record->transaction?->happened_on?->format('Y-m-d')
|
||||
?? $record->journal?->happened_on?->format('Y-m-d')
|
||||
?? $record->created_at->format('Y-m-d')),
|
||||
Column::make('reference')
|
||||
->heading('Reference')
|
||||
->formatStateUsing(fn ($record) => match(true) {
|
||||
$record->transaction && $record->transaction->transactionable =>
|
||||
$record->transaction->transactionable->voucher_number
|
||||
?? $record->transaction->transactionable->reference_number
|
||||
?? 'TR-'.$record->transaction->id,
|
||||
$record->transaction => 'TR-'.$record->transaction->id,
|
||||
$record->journal => $record->journal->series ?? 'JV-'.$record->journal->id,
|
||||
default => 'L-'.$record->id,
|
||||
}),
|
||||
Column::make('account.account')->heading('Account'),
|
||||
Column::make('description')->heading('Description'),
|
||||
Column::make('debit_amount')->heading('Debit'),
|
||||
Column::make('credit_amount')->heading('Credit'),
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
100
app/Filament/Resources/ClientResource/Pages/TrialBalance.php
Normal file
100
app/Filament/Resources/ClientResource/Pages/TrialBalance.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ClientResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ClientResource;
|
||||
use App\Models\Account;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\Summarizers\Summarizer;
|
||||
use Filament\Tables\Columns\Summarizers\Sum;
|
||||
use Filament\Resources\Pages\Concerns\InteractsWithRecord;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class TrialBalance extends Page implements HasTable
|
||||
{
|
||||
use InteractsWithTable;
|
||||
use InteractsWithRecord;
|
||||
|
||||
protected static string $resource = ClientResource::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-scale';
|
||||
|
||||
protected static string $view = 'filament.resources.client-resource.pages.trial-balance';
|
||||
|
||||
public function mount(int | string $record): void
|
||||
{
|
||||
$this->record = $this->resolveRecord($record);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query(
|
||||
Account::query()
|
||||
->where('client_id', $this->getRecord()->id)
|
||||
->with(['accountType', 'latestBalance'])
|
||||
)
|
||||
->columns([
|
||||
TextColumn::make('account')
|
||||
->label('Account Name')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
TextColumn::make('debit')
|
||||
->label('Debit')
|
||||
->money('PHP')
|
||||
->state(function (Account $record) {
|
||||
$balance = $record->latestBalance?->balance ?? 0;
|
||||
$normal = strtolower($record->accountType->normal_balance ?? 'debit');
|
||||
|
||||
return ($normal == 'debit' && $balance >= 0) || ($normal == 'credit' && $balance < 0)
|
||||
? abs($balance) : 0;
|
||||
})
|
||||
->summarize(Summarizer::make()
|
||||
->label('Total Debit')
|
||||
->money('PHP')
|
||||
->using(function ($query) {
|
||||
return Account::query()
|
||||
->whereIn('id', $query->clone()->pluck('accounts.id'))
|
||||
->with(['accountType', 'latestBalance'])
|
||||
->get()
|
||||
->sum(function ($record) {
|
||||
$balance = $record->latestBalance?->balance ?? 0;
|
||||
$normal = strtolower($record->accountType->normal_balance ?? 'debit');
|
||||
return ($normal == 'debit' && $balance >= 0) || ($normal == 'credit' && $balance < 0)
|
||||
? abs($balance) : 0;
|
||||
});
|
||||
})
|
||||
),
|
||||
TextColumn::make('credit')
|
||||
->label('Credit')
|
||||
->money('PHP')
|
||||
->state(function (Account $record) {
|
||||
$balance = $record->latestBalance?->balance ?? 0;
|
||||
$normal = strtolower($record->accountType->normal_balance ?? 'debit');
|
||||
|
||||
return ($normal == 'credit' && $balance >= 0) || ($normal == 'debit' && $balance < 0)
|
||||
? abs($balance) : 0;
|
||||
})
|
||||
->summarize(Summarizer::make()
|
||||
->label('Total Credit')
|
||||
->money('PHP')
|
||||
->using(function ($query) {
|
||||
return Account::query()
|
||||
->whereIn('id', $query->clone()->pluck('accounts.id'))
|
||||
->with(['accountType', 'latestBalance'])
|
||||
->get()
|
||||
->sum(function ($record) {
|
||||
$balance = $record->latestBalance?->balance ?? 0;
|
||||
$normal = strtolower($record->accountType->normal_balance ?? 'debit');
|
||||
return ($normal == 'credit' && $balance >= 0) || ($normal == 'debit' && $balance < 0)
|
||||
? abs($balance) : 0;
|
||||
});
|
||||
})
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,38 +2,238 @@
|
||||
|
||||
namespace App\Filament\Resources\ClientResource\RelationManagers;
|
||||
|
||||
use App\Actions\Transactions\CreateTransactionAction;
|
||||
use App\DataObjects\CreateTransactionDTO;
|
||||
use App\Models\Account;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Client;
|
||||
use Awcodes\TableRepeater\Components\TableRepeater;
|
||||
use Awcodes\TableRepeater\Header;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Pipeline;
|
||||
|
||||
class ExpensesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'expenses';
|
||||
|
||||
protected static ?string $title = 'Expenses';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('client_id')
|
||||
Select::make('branch_id')
|
||||
->relationship('branch', 'code', fn (Builder $query) => $query->where('client_id', $this->getOwnerRecord()->id))
|
||||
->afterStateUpdated(function ($set, $get) {
|
||||
$set('voucher_number', static::getVoucherNumber($get));
|
||||
$set('transactions.*.branch_id', $get('branch_id'));
|
||||
})
|
||||
->required()
|
||||
->maxLength(255),
|
||||
->live(),
|
||||
TextInput::make('supplier')->label('Supplier Name')->required(),
|
||||
TextInput::make('reference_number')->label('Reference Number'),
|
||||
TextInput::make('voucher_number')->label('Voucher Number')
|
||||
->default(fn ($get) => static::getVoucherNumber($get))
|
||||
->readOnly(),
|
||||
DatePicker::make('happened_on')->label('Date')
|
||||
->required()
|
||||
->afterStateUpdated(function ($set, $get) {
|
||||
$set('transactions.*.happened_on', $get('happened_on'));
|
||||
})
|
||||
->live()
|
||||
->native(false),
|
||||
|
||||
TableRepeater::make('transactions')
|
||||
->headers(fn (Get $get): array => static::getTransactionTableHeader($get))
|
||||
->relationship('transactions')
|
||||
->schema(fn (Get $get): array => $this->getTransactionTableFormSchema($get))
|
||||
->visible(fn (Get $get) => $get('branch_id') != null)
|
||||
->columnSpan('full'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getVoucherNumber(Get $get): string
|
||||
{
|
||||
$branch = Branch::find($get('branch_id'));
|
||||
|
||||
if ($branch) {
|
||||
return GenerateVoucher::execute($branch);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function getTransactionTableHeader(Get $get): array
|
||||
{
|
||||
return [
|
||||
Header::make('Charge Account'),
|
||||
Header::make('Description'),
|
||||
Header::make('Gross Amount'),
|
||||
Header::make('Exempt'),
|
||||
Header::make('Zero Rated'),
|
||||
Header::make('Vatable Amount'),
|
||||
Header::make('Input Tax'),
|
||||
Header::make('Withholding Tax'),
|
||||
Header::make('Net Amount'),
|
||||
];
|
||||
}
|
||||
|
||||
private function getTransactionTableFormSchema(Get $get): array
|
||||
{
|
||||
return [
|
||||
Select::make('account_id')->options(fn ($get) => $this->getAccountOptions($get)),
|
||||
TextInput::make('description')->label('Description'),
|
||||
Hidden::make('branch_id')->default(fn (Get $get) => $get('../../branch_id')),
|
||||
TextInput::make('gross_amount')
|
||||
->numeric()
|
||||
->live(false, 500)
|
||||
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
|
||||
$this->setDefaultFormValues($get, $set, $old, $state);
|
||||
})->default(0),
|
||||
TextInput::make('exempt')
|
||||
->numeric()
|
||||
->live()
|
||||
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
|
||||
$this->setDefaultFormValues($get, $set, $old, $state);
|
||||
})->default(0),
|
||||
TextInput::make('zero_rated')
|
||||
->numeric()
|
||||
->live()
|
||||
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
|
||||
|
||||
$this->setDefaultFormValues($get, $set, $old, $state);
|
||||
})->default(0),
|
||||
TextInput::make('vatable_amount')
|
||||
->numeric()
|
||||
->nullable()
|
||||
->live()
|
||||
->readOnly()
|
||||
->default(0),
|
||||
Hidden::make('happened_on')->default(fn (Get $get) => $get('../../happened_on')),
|
||||
TextInput::make('input_tax')
|
||||
->numeric()
|
||||
->live()
|
||||
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
|
||||
|
||||
$this->setDefaultFormValues($get, $set, $old, $state);
|
||||
})->default(0),
|
||||
TextInput::make('payable_withholding_tax')
|
||||
->numeric()
|
||||
->live()
|
||||
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
|
||||
|
||||
$this->setDefaultFormValues($get, $set, $old, $state);
|
||||
})->default(0),
|
||||
TextInput::make('net_amount')->numeric()->default(0),
|
||||
];
|
||||
}
|
||||
|
||||
private function getAccountOptions(Get $get): Collection
|
||||
{
|
||||
$query = Account::query();
|
||||
|
||||
$query->where([
|
||||
'client_id' => $this->getOwnerRecord()->id,
|
||||
]);
|
||||
|
||||
if ($get('../../branch_id')) {
|
||||
$query->whereHas('balances', function ($query) use ($get) {
|
||||
return $query->where('branch_id', $get('../../branch_id'));
|
||||
});
|
||||
}
|
||||
|
||||
$query->whereHas('accountType', function ($query) {
|
||||
return $query->where('type', 'Expenses');
|
||||
});
|
||||
|
||||
return $query->get()->pluck('account', 'id');
|
||||
}
|
||||
|
||||
private function setDefaultFormValues(Get $get, Set $set, ?string $old, ?string $state): void
|
||||
{
|
||||
$exempt = (float) $get('exempt');
|
||||
$withHoldingTax = (float) $get('payable_withholding_tax');
|
||||
$vatableSales = $get('gross_amount');
|
||||
$vatableAmount = 0;
|
||||
if ($vatableSales) {
|
||||
$vatableAmount = $vatableSales / 1.12;
|
||||
}
|
||||
|
||||
$inputTax = $vatableAmount * 0.12;
|
||||
|
||||
$netAmount = (int) $vatableSales - $get('payable_withholding_tax');
|
||||
|
||||
if ($this->getOwnerRecord()->vatable) {
|
||||
$netAmount = ($vatableAmount + $exempt) - $withHoldingTax;
|
||||
}
|
||||
|
||||
$set('input_tax', number_format($inputTax, 2, '.', ''));
|
||||
$set('vatable_amount', number_format($vatableAmount, 2, '.', ''));
|
||||
$set('net_amount', number_format($netAmount, 2, '.', ''));
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('client_id')
|
||||
->recordTitleAttribute('supplier')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('client_id'),
|
||||
TextColumn::make('supplier'),
|
||||
TextColumn::make('reference_number'),
|
||||
TextColumn::make('voucher_number'),
|
||||
TextColumn::make('branch.code'),
|
||||
TextColumn::make('happened_on'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make(),
|
||||
Tables\Actions\CreateAction::make()
|
||||
->using(function (array $data, string $model) {
|
||||
$transactions = $data['transactions'] ?? [];
|
||||
$data = Arr::except($data, ['transactions']);
|
||||
|
||||
$record = $model::create($data);
|
||||
|
||||
try {
|
||||
$branch = $record->branch;
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
|
||||
$tData = [
|
||||
'branch_id' => $branch->id,
|
||||
'happened_on' => $record->happened_on,
|
||||
...$transaction,
|
||||
];
|
||||
|
||||
$payload = new CreateTransactionDTO(data: $tData, transactionable: $record);
|
||||
|
||||
Pipeline::send(passable: $payload)->through(
|
||||
[
|
||||
CreateTransactionAction::class,
|
||||
]
|
||||
)->thenReturn();
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
throw new \Exception('Failed to save transactions : '.$exception->getMessage());
|
||||
}
|
||||
|
||||
return $record;
|
||||
}),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ClientResource\RelationManagers;
|
||||
|
||||
use App\Actions\Balances\CreateBalanceAction;
|
||||
use App\Actions\Ledgers\CreateLedgerAction;
|
||||
use App\DataObjects\CreateLedgerDTO;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Journal;
|
||||
use Awcodes\TableRepeater\Components\TableRepeater;
|
||||
use Awcodes\TableRepeater\Header;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Pipeline;
|
||||
|
||||
class JournalsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'journals';
|
||||
|
||||
protected static ?string $title = 'Journal Entries (Adjustments)';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Select::make('branch_id')
|
||||
->label('Branch')
|
||||
->options(fn () => Branch::where('client_id', $this->getOwnerRecord()->id)->pluck('name', 'id'))
|
||||
->required()
|
||||
->default(fn () => Branch::where('client_id', $this->getOwnerRecord()->id)->first()?->id),
|
||||
|
||||
DatePicker::make('happened_on')
|
||||
->label('Date')
|
||||
->required()
|
||||
->default(now()),
|
||||
|
||||
TextInput::make('series')
|
||||
->label('Reference/Series #')
|
||||
->required(),
|
||||
|
||||
Textarea::make('description')
|
||||
->label('Description')
|
||||
->columnSpanFull(),
|
||||
|
||||
TableRepeater::make('ledgers')
|
||||
->relationship('ledgers')
|
||||
->headers([
|
||||
Header::make('Account'),
|
||||
Header::make('Description'),
|
||||
Header::make('Debit'),
|
||||
Header::make('Credit'),
|
||||
])
|
||||
->schema([
|
||||
Select::make('account_id')
|
||||
->relationship('account', 'account', fn (Builder $query) => $query->where('client_id', $this->getOwnerRecord()->id))
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
TextInput::make('description'),
|
||||
TextInput::make('debit_amount')
|
||||
->numeric()
|
||||
->default(0),
|
||||
TextInput::make('credit_amount')
|
||||
->numeric()
|
||||
->default(0),
|
||||
])
|
||||
->columnSpanFull()
|
||||
->minItems(2)
|
||||
->live()
|
||||
->rules([
|
||||
fn (): \Closure => function (string $attribute, $value, \Closure $fail) {
|
||||
$debit = collect($value)->sum('debit_amount');
|
||||
$credit = collect($value)->sum('credit_amount');
|
||||
if (abs($debit - $credit) > 0.01) {
|
||||
$fail("Total Debit (" . number_format($debit, 2) . ") must equal Total Credit (" . number_format($credit, 2) . ").");
|
||||
}
|
||||
},
|
||||
])
|
||||
->afterStateUpdated(function ($state, $component) {
|
||||
// Optional: Validation logic
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('description')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('happened_on')
|
||||
->date()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('series')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('description')
|
||||
->limit(50),
|
||||
Tables\Columns\TextColumn::make('total_debit')
|
||||
->label('Total Debit')
|
||||
->state(fn (Journal $record) => $record->ledgers->sum('debit_amount'))
|
||||
->money('PHP'),
|
||||
Tables\Columns\TextColumn::make('total_credit')
|
||||
->label('Total Credit')
|
||||
->state(fn (Journal $record) => $record->ledgers->sum('credit_amount'))
|
||||
->money('PHP'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make()
|
||||
->label('Add Adjustment Entry')
|
||||
->using(function (array $data, string $model) {
|
||||
return DB::transaction(function () use ($data, $model) {
|
||||
$ledgersData = $data['ledgers'] ?? [];
|
||||
$journalData = Arr::except($data, ['ledgers']);
|
||||
$journalData['client_id'] = $this->getOwnerRecord()->id;
|
||||
|
||||
$journal = $model::create($journalData);
|
||||
|
||||
foreach ($ledgersData as $ledger) {
|
||||
$ledgerPayload = new CreateLedgerDTO(
|
||||
branch_id: $journal->branch_id,
|
||||
amount: ($ledger['debit_amount'] > 0) ? $ledger['debit_amount'] : $ledger['credit_amount'],
|
||||
ledger: null, // Will be created
|
||||
transaction: null, // No transaction
|
||||
journal: $journal,
|
||||
account: \App\Models\Account::find($ledger['account_id']),
|
||||
type: ($ledger['debit_amount'] > 0) ? 'debit' : 'credit'
|
||||
);
|
||||
|
||||
Pipeline::send(passable: $ledgerPayload)->through(
|
||||
[
|
||||
CreateLedgerAction::class,
|
||||
]
|
||||
)->thenReturn();
|
||||
}
|
||||
return $journal;
|
||||
});
|
||||
}),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ClientResource\RelationManagers;
|
||||
|
||||
use App\Actions\Transactions\CreateTransactionAction;
|
||||
use App\DataObjects\CreateTransactionDTO;
|
||||
use Illuminate\Support\Facades\Pipeline;
|
||||
use App\Models\Account;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Client;
|
||||
use Awcodes\TableRepeater\Components\TableRepeater;
|
||||
use Awcodes\TableRepeater\Header;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SalesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'sales';
|
||||
|
||||
protected static ?string $title = 'Sales';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Select::make('branch_id')
|
||||
->relationship('branch', 'code', fn (Builder $query) => $query->where('client_id', $this->getOwnerRecord()->id))
|
||||
->required()
|
||||
->afterStateUpdated(function ($set, $get) {
|
||||
$set('current_series', static::getSeries($get));
|
||||
$set('transactions.*.branch_id', $get('branch_id'));
|
||||
})
|
||||
->live(),
|
||||
TextInput::make('current_series')
|
||||
->label('Series')
|
||||
->disabled(),
|
||||
DatePicker::make('happened_on')->label('Date')
|
||||
->required()
|
||||
->afterStateUpdated(function ($set, $get) {
|
||||
$set('transactions.*.happened_on', $get('happened_on'));
|
||||
})
|
||||
->native(false),
|
||||
Checkbox::make('with_discount')->label('With Discount?')->default(false)->live(),
|
||||
|
||||
TableRepeater::make('transactions')
|
||||
->headers(fn (Get $get): array => static::getTransactionTableHeader($get))
|
||||
->relationship('transactions')
|
||||
->schema(fn (Get $get): array => $this->getTransactionTableFormSchema($get))
|
||||
->visible(fn (Get $get) => $get('branch_id') != null)
|
||||
->columnSpan('full'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getSeries(Get $get): string
|
||||
{
|
||||
$branch = Branch::find($get('branch_id'));
|
||||
|
||||
if ($branch) {
|
||||
$currentSeries = $branch->current_series;
|
||||
|
||||
return str_pad($currentSeries + 1, 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
}
|
||||
|
||||
private static function getTransactionTableHeader(Get $get): array
|
||||
{
|
||||
if ($get('with_discount')) {
|
||||
return [
|
||||
Header::make('Charge Account'),
|
||||
Header::make('Description'),
|
||||
Header::make('Gross Amount'),
|
||||
Header::make('Exempt'),
|
||||
Header::make('Vatable Amount'),
|
||||
Header::make('Output Tax'),
|
||||
Header::make('Withholding Tax'),
|
||||
Header::make('Discount'),
|
||||
Header::make('Net Amount'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
Header::make('Charge Account'),
|
||||
Header::make('Description'),
|
||||
Header::make('Gross Amount'),
|
||||
Header::make('Exempt'),
|
||||
Header::make('Vatable Amount'),
|
||||
Header::make('Output Tax'),
|
||||
Header::make('Withholding Tax'),
|
||||
Header::make('Net Amount'),
|
||||
];
|
||||
}
|
||||
|
||||
private function getTransactionTableFormSchema(Get $get): array
|
||||
{
|
||||
return [
|
||||
Select::make('account_id')->options(fn ($get) => $this->getAccountOptions($get)),
|
||||
TextInput::make('description')->label('Description'),
|
||||
Hidden::make('branch_id')->default(fn (Get $get) => $get('../../branch_id')),
|
||||
TextInput::make('gross_amount')
|
||||
->numeric()
|
||||
->live(false, 500)
|
||||
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
|
||||
$this->setDefaultFormValues($get, $set, $old, $state);
|
||||
})->default(0),
|
||||
TextInput::make('exempt')
|
||||
->numeric()
|
||||
->live()
|
||||
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
|
||||
$this->setDefaultFormValues($get, $set, $old, $state);
|
||||
})->default(0),
|
||||
TextInput::make('vatable_amount')
|
||||
->numeric()
|
||||
->nullable()
|
||||
->live()
|
||||
->readOnly()
|
||||
->default(0),
|
||||
Hidden::make('happened_on')->default(fn (Get $get) => $get('../../happened_on')),
|
||||
Hidden::make('with_discount')->default(fn (Get $get) => $get('../../with_discount')),
|
||||
TextInput::make('output_tax')
|
||||
->numeric()
|
||||
->live()
|
||||
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
|
||||
|
||||
$this->setDefaultFormValues($get, $set, $old, $state);
|
||||
})->default(0),
|
||||
TextInput::make('payable_withholding_tax')
|
||||
->numeric()
|
||||
->live()
|
||||
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
|
||||
|
||||
$this->setDefaultFormValues($get, $set, $old, $state);
|
||||
})->default(0),
|
||||
TextInput::make('discount')
|
||||
->numeric()
|
||||
->readOnly()
|
||||
->visible(fn (Get $get) => $get('../../with_discount'))
|
||||
->live(),
|
||||
TextInput::make('net_amount')->numeric()->default(0),
|
||||
];
|
||||
}
|
||||
|
||||
private function getAccountOptions($get)
|
||||
{
|
||||
$query = Account::query();
|
||||
|
||||
$query->where([
|
||||
'client_id' => $this->getOwnerRecord()->id,
|
||||
]);
|
||||
|
||||
if ($get('../../branch_id')) {
|
||||
$query->whereHas('balances', function ($query) use ($get) {
|
||||
return $query->where('branch_id', $get('../../branch_id'));
|
||||
});
|
||||
}
|
||||
|
||||
$query->whereHas('accountType', function ($query) {
|
||||
return $query->where('type', 'Revenue');
|
||||
});
|
||||
|
||||
return $query->get()->pluck('account', 'id');
|
||||
}
|
||||
|
||||
private function setDefaultFormValues(Get $get, Set $set, ?string $old, ?string $state)
|
||||
{
|
||||
$exempt = (float) $get('exempt');
|
||||
$withHoldingTax = (float) $get('payable_withholding_tax');
|
||||
$vatableSales = $get('gross_amount');
|
||||
$vatableAmount = 0;
|
||||
if ($vatableSales) {
|
||||
$vatableAmount = $vatableSales / 1.12;
|
||||
}
|
||||
|
||||
$discount = $exempt * .20;
|
||||
$outputTax = $vatableAmount * 0.12;
|
||||
|
||||
//default net amount
|
||||
$netAmount = (int) $vatableSales - $get('payable_withholding_tax');
|
||||
|
||||
//net amount if vatable
|
||||
if ($this->getOwnerRecord()->vatable) {
|
||||
$netAmount = ($vatableAmount + $exempt) - $withHoldingTax;
|
||||
}
|
||||
|
||||
//if discounted
|
||||
if ($get('../../with_discount')) {
|
||||
$netAmount = $netAmount - $discount;
|
||||
}
|
||||
|
||||
$set('output_tax', number_format($outputTax, 2, '.', ''));
|
||||
$set('discount', number_format($discount, 2, '.', ''));
|
||||
$set('vatable_amount', number_format($vatableAmount, 2, '.', ''));
|
||||
$set('net_amount', number_format($netAmount, 2, '.', ''));
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('title')
|
||||
->columns([
|
||||
TextColumn::make('id')->label('ID')->sortable(),
|
||||
TextColumn::make('branch.code')->label('Branch')->sortable(),
|
||||
TextColumn::make('happened_on')->label('Date')->date()->sortable(),
|
||||
TextColumn::make('gross_amount')->label('Gross Amount')->numeric()->sortable(),
|
||||
TextColumn::make('exempt')->label('Exempt')->numeric()->sortable(),
|
||||
TextColumn::make('vatable_amount')->label('Vatable Amount')->numeric()->sortable(),
|
||||
TextColumn::make('output_tax')->label('Output Tax')->numeric()->sortable(),
|
||||
TextColumn::make('payable_withholding_tax')->label('Payable Withholding Tax')->numeric()->sortable(),
|
||||
TextColumn::make('discount')->label('Discount')->numeric()->sortable(),
|
||||
TextColumn::make('net_amount')->label('Net Amount')->numeric()->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make()
|
||||
->using(function (array $data, string $model) {
|
||||
$transactions = $data['transactions'] ?? [];
|
||||
$data = Arr::except($data, ['transactions']);
|
||||
|
||||
$record = $model::create($data);
|
||||
|
||||
try {
|
||||
$branch = $record->branch;
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
$tData = [
|
||||
'branch_id' => $branch->id,
|
||||
'happened_on' => $record->happened_on,
|
||||
...$transaction,
|
||||
];
|
||||
|
||||
$payload = new CreateTransactionDTO(data: $tData, transactionable: $record);
|
||||
|
||||
Pipeline::send(passable: $payload)->through(
|
||||
[
|
||||
CreateTransactionAction::class,
|
||||
]
|
||||
)->thenReturn();
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
throw new \Exception('Failed to save transactions : '.$exception->getMessage());
|
||||
}
|
||||
|
||||
return $record;
|
||||
}),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,9 @@ class ExpenseResource extends Resource
|
||||
|
||||
protected static bool $isVatable;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
protected static ?string $navigationIcon = 'heroicon-o-banknotes';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
|
||||
@@ -19,13 +19,16 @@ use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class SaleResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Sale::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
@@ -215,7 +218,17 @@ class SaleResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
//
|
||||
TextColumn::make('id')->label('ID')->sortable(),
|
||||
TextColumn::make('client.name')->label('Client')->sortable(),
|
||||
TextColumn::make('branch.name')->label('Branch')->sortable(),
|
||||
TextColumn::make('happened_on')->label('Date')->date()->sortable(),
|
||||
TextColumn::make('gross_amount')->label('Gross Amount')->numeric()->sortable(),
|
||||
TextColumn::make('exempt')->label('Exempt')->numeric()->sortable(),
|
||||
TextColumn::make('vatable_amount')->label('Vatable Amount')->numeric()->sortable(),
|
||||
TextColumn::make('output_tax')->label('Output Tax')->numeric()->sortable(),
|
||||
TextColumn::make('payable_withholding_tax')->label('Payable Withholding Tax')->numeric()->sortable(),
|
||||
TextColumn::make('discount')->label('Discount')->numeric()->sortable(),
|
||||
TextColumn::make('net_amount')->label('Net Amount')->numeric()->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
|
||||
@@ -24,6 +24,5 @@ class CreateSale extends CreateRecord
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$branch = Branch::find($this->data['branch_id']);
|
||||
dd($branch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,16 @@ class Account extends Model
|
||||
return $this->hasMany(Balance::class);
|
||||
}
|
||||
|
||||
public function latestBalance(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(Balance::class)->latestOfMany();
|
||||
}
|
||||
|
||||
public function ledgers(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ledger::class);
|
||||
}
|
||||
|
||||
public function getCurrentBalanceAttribute()
|
||||
{
|
||||
if ($this->balances()->exists()) {
|
||||
|
||||
@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
|
||||
#[ObservedBy([ClientObserver::class])]
|
||||
class Client extends Model
|
||||
@@ -62,4 +63,14 @@ class Client extends Model
|
||||
{
|
||||
return $this->hasMany(Transmittal::class);
|
||||
}
|
||||
|
||||
public function sales(): HasManyThrough
|
||||
{
|
||||
return $this->hasManyThrough(Sale::class, Branch::class);
|
||||
}
|
||||
|
||||
public function expenses(): HasManyThrough
|
||||
{
|
||||
return $this->hasManyThrough(Expense::class, Branch::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use App\Traits\HasUser;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Journal extends Model
|
||||
{
|
||||
@@ -17,9 +17,9 @@ class Journal extends Model
|
||||
|
||||
protected $casts = ['happened_on' => 'date'];
|
||||
|
||||
public function ledger(): HasOne
|
||||
public function ledgers(): HasMany
|
||||
{
|
||||
return $this->hasOne(Ledger::class);
|
||||
return $this->hasMany(Ledger::class);
|
||||
}
|
||||
|
||||
public function scopeDateCreatedFilter(Builder $query, $date) : Builder
|
||||
|
||||
@@ -15,6 +15,10 @@ class Transaction extends Model
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'happened_on' => 'date',
|
||||
];
|
||||
|
||||
public function transactionable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
|
||||
@@ -22,6 +22,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Gate::before(function ($user, $ability) {
|
||||
return $user->hasRole('super_admin') ? true : null;
|
||||
});
|
||||
|
||||
Gate::policy(Role::class, RolePolicy::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/boost": "^2.1",
|
||||
"laravel/breeze": "^2.1",
|
||||
"laravel/pint": "^1.13",
|
||||
"laravel/sail": "^1.26",
|
||||
|
||||
4803
composer.lock
generated
4803
composer.lock
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{["x-on:blur"]:"createTag()",["x-model"]:"newTag",["x-on:keydown"](t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},["x-on:paste"](){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default};
|
||||
function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{"x-on:blur":"createTag()","x-model":"newTag","x-on:keydown"(t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},"x-on:paste"(){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default};
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
function n(){return{checkboxClickController:null,collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){if(this.isLoading=!0,this.areRecordsSelected(this.getRecordsInGroupOnPage(e))){this.deselectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e));return}this.selectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e)),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){this.checkboxClickController&&this.checkboxClickController.abort(),this.checkboxClickController=new AbortController;let{signal:e}=this.checkboxClickController;this.$root?.addEventListener("click",t=>t.target?.matches(".fi-ta-record-checkbox")&&this.handleCheckboxClick(t,t.target),{signal:e})},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let o=s.indexOf(this.lastChecked),r=s.indexOf(t),l=[o,r].sort((i,d)=>i-d),c=[];for(let i=l[0];i<=l[1];i++)s[i].checked=t.checked,c.push(s[i].value);t.checked?this.selectRecords(c):this.deselectRecords(c)}this.lastChecked=t}}}export{n as default};
|
||||
function d(){return{checkboxClickController:null,collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){this.isLoading=!0;let t=await this.$wire.getGroupedSelectableTableRecordKeys(e);this.areRecordsSelected(this.getRecordsInGroupOnPage(e))?this.deselectRecords(t):this.selectRecords(t),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){this.checkboxClickController&&this.checkboxClickController.abort(),this.checkboxClickController=new AbortController;let{signal:e}=this.checkboxClickController;this.$root?.addEventListener("click",t=>t.target?.matches(".fi-ta-record-checkbox")&&this.handleCheckboxClick(t,t.target),{signal:e})},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let l=s.indexOf(this.lastChecked),r=s.indexOf(t),o=[l,r].sort((c,n)=>c-n),i=[];for(let c=o[0];c<=o[1];c++)s[c].checked=t.checked,i.push(s[c].value);t.checked?this.selectRecords(i):this.deselectRecords(i)}this.lastChecked=t}}}export{d as default};
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,69 +0,0 @@
|
||||
<x-filament-panels::page>
|
||||
|
||||
|
||||
<div class="relative overflow-x-auto">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Product name
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Color
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Category
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Price
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
Apple MacBook Pro 17"
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
Silver
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
Laptop
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
$2999
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
Microsoft Surface Pro
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
White
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
Laptop PC
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
$1999
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-white dark:bg-gray-800">
|
||||
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
Magic Mouse 2
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
Black
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
Accessories
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
$99
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</x-filament-panels::page>
|
||||
@@ -1,45 +0,0 @@
|
||||
<x-filament-panels::page>
|
||||
<div class="relative overflow-x-auto">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Account
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Debit Amount
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Credit Amount
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
Apple MacBook Pro 17"
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
$2999
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
$2999
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th scope="row" class="px-6 py-4 font-medium text-red-700 whitespace-nowrap dark:text-red-700">
|
||||
Total
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
$1999
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
$1999
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</x-filament-panels::page>
|
||||
@@ -0,0 +1,3 @@
|
||||
<x-filament-panels::page>
|
||||
{{ $this->table }}
|
||||
</x-filament-panels::page>
|
||||
@@ -0,0 +1,3 @@
|
||||
<x-filament-panels::page>
|
||||
{{ $this->table }}
|
||||
</x-filament-panels::page>
|
||||
62
trialbalance.md
Normal file
62
trialbalance.md
Normal file
@@ -0,0 +1,62 @@
|
||||
I need to add comprehensive trial balance functionality to my Filament application with adjustment capabilities:
|
||||
|
||||
1. Create models for:
|
||||
- TrialBalance (main trial balance report)
|
||||
- TrialBalanceAdjustment (for adjusting entries)
|
||||
- SuspenseAccount (temporary holding for discrepancies)
|
||||
|
||||
2. TrialBalance fields:
|
||||
- period_start_date, period_end_date
|
||||
- account_code, account_name
|
||||
- debit_balance, credit_balance
|
||||
- account_type (asset, liability, equity, revenue, expense)
|
||||
- is_adjusted (boolean)
|
||||
- created_at, updated_at
|
||||
|
||||
3. TrialBalanceAdjustment fields:
|
||||
- trial_balance_id (foreign key)
|
||||
- adjustment_type (enum: accrual, deferral, depreciation, correction, suspense)
|
||||
- account_code
|
||||
- description
|
||||
- debit_amount, credit_amount
|
||||
- adjustment_date
|
||||
- created_by (user_id)
|
||||
- status (enum: draft, posted, reversed)
|
||||
|
||||
4. Filament Resource features:
|
||||
- Main trial balance table showing all accounts with balances
|
||||
- Summary footer showing:
|
||||
* Total Debits
|
||||
* Total Credits
|
||||
* Difference (highlighted in red if non-zero)
|
||||
- Action button "Add Adjustment Entry" that opens a modal form
|
||||
- Adjustment form with:
|
||||
* Account selection dropdown
|
||||
* Adjustment type selector
|
||||
* Debit/Credit amount inputs (with validation that one must be zero)
|
||||
* Description field
|
||||
* Auto-calculation of new balance
|
||||
- "Create Suspense Entry" button for unexplained differences
|
||||
- Table filter for: adjusted/unadjusted entries, date range, account type
|
||||
- Relation manager showing all adjustments for each account
|
||||
- Color coding: green when balanced, red when unbalanced
|
||||
- "Post Adjustments" action to finalize entries
|
||||
- "Adjusted Trial Balance" view showing balances after adjustments
|
||||
|
||||
5. Additional features:
|
||||
- Audit trail for all adjustments
|
||||
- Ability to reverse adjustments
|
||||
- Automatic journal entry creation from adjustments
|
||||
- Export to PDF/Excel with adjustments detailed
|
||||
- Before/after comparison view
|
||||
- Permission checks (only accountants can add adjustments)
|
||||
|
||||
6. Include these methods:
|
||||
- calculateTrialBalance() - generates from ledger
|
||||
- addAdjustment() - creates adjustment entry
|
||||
- postAdjustments() - finalizes and posts to general ledger
|
||||
- reverseAdjustment() - reverses a posted adjustment
|
||||
- getAdjustedBalance() - calculates balance after adjustments
|
||||
- checkBalance() - verifies debits equal credits
|
||||
|
||||
Please provide the complete code including models, migrations, Fila ment resources, and form components.
|
||||
Reference in New Issue
Block a user