3 Commits

Author SHA1 Message Date
Jp
0131193b8d refactor: streamline sales and expenses management in client resource
- Move create/edit logic from relation managers to dedicated resource pages
- Add transaction handling with proper rollback in sale create/update
- Fix expense transaction creation by using correct array access
- Set default client from query parameter in sale/expense forms
- Exclude 'type' field from balance creation to prevent errors
2026-02-10 15:05:36 +08:00
Jp
0e4da559d6 fix: cascade delete related transactions and ledgers for sales and expenses
Ensure data integrity by cleaning up dependent records when deleting Sale or Expense models. This prevents orphaned transactions and ledgers in the database.
2026-02-09 22:25:08 +08:00
Jp
1548e178bc feat(client): add journals relation and improve transaction ledger handling
- Add journals relation to Client model via Branch
- Update branch selection in JournalsRelationManager to use code instead of name
- Improve tax, withholding, and cash account ledger queries by adding client_id filter and amount checks
- Add missing import for GenerateVoucher command in ExpensesRelationManager
- Label 'Normal Balance' column in AccountsRelationManager
2026-02-09 22:16:10 +08:00
14 changed files with 184 additions and 441 deletions

View File

@@ -15,7 +15,7 @@ class CreateBalanceAction extends BaseAction
public function __invoke(Data $payload, Closure $next) public function __invoke(Data $payload, Closure $next)
{ {
$this->createBalanceCommand->execute($payload->balanceDTO->except('amount')->toArray()); $this->createBalanceCommand->execute($payload->balanceDTO->except('amount', 'type')->toArray());
return $next($payload); return $next($payload);
} }

View File

@@ -71,15 +71,21 @@ class CreateTransactionAction extends BaseAction
{ {
$accountName = $isExpense ? 'Input Tax' : 'Output Tax'; $accountName = $isExpense ? 'Input Tax' : 'Output Tax';
$type = $isExpense ? 'debit' : 'credit'; $type = $isExpense ? 'debit' : 'credit';
$clientId = $payload->transactionable->branch->client_id;
$taxAccount = Account::query()->where('account', $accountName)->whereHas('balances', function ($balance) use ($payload) { $taxAccount = Account::query()
return $balance->where('branch_id', $payload->transactionable->branch_id); ->where('account', $accountName)
})->first(); ->where('client_id', $clientId)
->first();
if ($taxAccount) { $amount = $isExpense
? ($payload->transactionable->input_tax ?? 0.00)
: ($payload->transactionable->output_tax ?? 0.00);
if ($taxAccount && $amount > 0) {
$ledgerPayload = new CreateLedgerDTO( $ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id, 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? amount: $amount,
transaction: $payload->transaction, transaction: $payload->transaction,
account: $taxAccount, account: $taxAccount,
type: $type, type: $type,
@@ -93,15 +99,19 @@ class CreateTransactionAction extends BaseAction
$isExpense = $payload->transactionable instanceof \App\Models\Expense; $isExpense = $payload->transactionable instanceof \App\Models\Expense;
$accountName = $isExpense ? 'Payable Withholding Tax' : 'Creditable Withholding Tax'; $accountName = $isExpense ? 'Payable Withholding Tax' : 'Creditable Withholding Tax';
$type = $isExpense ? 'credit' : 'debit'; $type = $isExpense ? 'credit' : 'debit';
$clientId = $payload->transactionable->branch->client_id;
$withholdingAccount = Account::query()->where('account', $accountName)->whereHas('balances', function ($balance) use ($payload) { $withholdingAccount = Account::query()
return $balance->where('branch_id', $payload->transactionable->branch_id); ->where('account', $accountName)
})->first(); ->where('client_id', $clientId)
->first();
if ($withholdingAccount) { $amount = $payload->transaction->payable_withholding_tax ?? 0.00;
if ($withholdingAccount && $amount > 0) {
$ledgerPayload = new CreateLedgerDTO( $ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id, branch_id: $payload->transactionable->branch_id,
amount: $payload->transaction->payable_withholding_tax ?? 0.00, amount: $amount,
transaction: $payload->transaction, transaction: $payload->transaction,
account: $withholdingAccount, account: $withholdingAccount,
type: $type, type: $type,
@@ -116,12 +126,14 @@ class CreateTransactionAction extends BaseAction
$type = $isExpense ? 'credit' : 'debit'; $type = $isExpense ? 'credit' : 'debit';
$wht = $isExpense ? ($payload->transaction->payable_withholding_tax ?? 0) : ($payload->transaction->creditable_withholding_tax ?? 0); $wht = $isExpense ? ($payload->transaction->payable_withholding_tax ?? 0) : ($payload->transaction->creditable_withholding_tax ?? 0);
$amount = ($payload->transaction->gross_amount ?? 0) - $wht; $amount = ($payload->transaction->gross_amount ?? 0) - $wht;
$clientId = $payload->transactionable->branch->client_id;
$cashAccount = Account::query()->where('account', 'Cash')->whereHas('balances', function ($balance) use ($payload) { $cashAccount = Account::query()
return $balance->where('branch_id', $payload->transactionable->branch_id); ->where('account', 'Cash')
})->first(); ->where('client_id', $clientId)
->first();
if ($cashAccount) { if ($cashAccount && $amount > 0) {
$ledgerPayload = new CreateLedgerDTO( $ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id, branch_id: $payload->transactionable->branch_id,
amount: $amount, amount: $amount,

View File

@@ -37,7 +37,7 @@ class AccountsRelationManager extends RelationManager
->columns([ ->columns([
Tables\Columns\TextColumn::make('account')->description(fn (Account $record): string => $record->description ?? ''), Tables\Columns\TextColumn::make('account')->description(fn (Account $record): string => $record->description ?? ''),
Tables\Columns\TextColumn::make('accountType.type'), Tables\Columns\TextColumn::make('accountType.type'),
Tables\Columns\TextColumn::make('accountType.normal_balance'), Tables\Columns\TextColumn::make('accountType.normal_balance')->label('Normal Balance'),
]) ])
->filters([ ->filters([
// //

View File

@@ -2,30 +2,13 @@
namespace App\Filament\Resources\ClientResource\RelationManagers; namespace App\Filament\Resources\ClientResource\RelationManagers;
use App\Actions\Transactions\CreateTransactionAction; use App\Filament\Resources\ExpenseResource;
use App\DataObjects\CreateTransactionDTO; use App\Models\Expense;
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\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; 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 class ExpensesRelationManager extends RelationManager
{ {
@@ -36,155 +19,7 @@ class ExpensesRelationManager extends RelationManager
public function form(Form $form): Form public function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([]);
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()
->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 public function table(Table $table): Table
@@ -203,40 +38,11 @@ class ExpensesRelationManager extends RelationManager
]) ])
->headerActions([ ->headerActions([
Tables\Actions\CreateAction::make() Tables\Actions\CreateAction::make()
->using(function (array $data, string $model) { ->url(fn () => ExpenseResource::getUrl('create', ['client_id' => $this->getOwnerRecord()->id])),
$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([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->url(fn (Expense $record) => ExpenseResource::getUrl('edit', ['record' => $record])),
Tables\Actions\DeleteAction::make(), Tables\Actions\DeleteAction::make(),
]) ])
->bulkActions([ ->bulkActions([

View File

@@ -35,7 +35,7 @@ class JournalsRelationManager extends RelationManager
->schema([ ->schema([
Select::make('branch_id') Select::make('branch_id')
->label('Branch') ->label('Branch')
->options(fn () => Branch::where('client_id', $this->getOwnerRecord()->id)->pluck('name', 'id')) ->options(fn () => Branch::where('client_id', $this->getOwnerRecord()->id)->pluck('code', 'id'))
->required() ->required()
->default(fn () => Branch::where('client_id', $this->getOwnerRecord()->id)->first()?->id), ->default(fn () => Branch::where('client_id', $this->getOwnerRecord()->id)->first()?->id),

View File

@@ -2,31 +2,14 @@
namespace App\Filament\Resources\ClientResource\RelationManagers; namespace App\Filament\Resources\ClientResource\RelationManagers;
use App\Actions\Transactions\CreateTransactionAction; use App\Filament\Resources\SaleResource;
use App\DataObjects\CreateTransactionDTO; use App\Models\Sale;
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;
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\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; 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 class SalesRelationManager extends RelationManager
{ {
@@ -36,178 +19,7 @@ class SalesRelationManager extends RelationManager
public function form(Form $form): Form public function form(Form $form): Form
{ {
return $form return $form->schema([]);
->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 public function table(Table $table): Table
@@ -231,39 +43,11 @@ class SalesRelationManager extends RelationManager
]) ])
->headerActions([ ->headerActions([
Tables\Actions\CreateAction::make() Tables\Actions\CreateAction::make()
->using(function (array $data, string $model) { ->url(fn () => SaleResource::getUrl('create', ['client_id' => $this->getOwnerRecord()->id])),
$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([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->url(fn (Sale $record) => SaleResource::getUrl('edit', ['record' => $record])),
Tables\Actions\DeleteAction::make(), Tables\Actions\DeleteAction::make(),
]) ])
->bulkActions([ ->bulkActions([

View File

@@ -43,6 +43,7 @@ class ExpenseResource extends Resource
{ {
return [ return [
Select::make('client') Select::make('client')
->default(request()->query('client_id'))
->options(Client::query()->get()->pluck('company', 'id')) ->options(Client::query()->get()->pluck('company', 'id'))
->afterStateUpdated(function ($set, $get) { ->afterStateUpdated(function ($set, $get) {
$set('branch_id', ''); $set('branch_id', '');

View File

@@ -28,7 +28,7 @@ class CreateExpense extends CreateRecord
protected function afterCreate(): void protected function afterCreate(): void
{ {
$transactions = Arr::only($this->form->getState(), ['transactions']); $transactions = $this->form->getState()['transactions'] ?? [];
try { try {
$branch = $this->getRecord()->branch; $branch = $this->getRecord()->branch;
@@ -38,7 +38,7 @@ class CreateExpense extends CreateRecord
$data = [ $data = [
'branch_id' => $branch->id, 'branch_id' => $branch->id,
'happened_on' => $this->getRecord()->happened_on, 'happened_on' => $this->getRecord()->happened_on,
...Arr::first($transaction), ...$transaction,
]; ];
$payload = new CreateTransactionDTO(data: $data, transactionable: $this->getRecord()); $payload = new CreateTransactionDTO(data: $data, transactionable: $this->getRecord());

View File

@@ -35,6 +35,7 @@ class SaleResource extends Resource
return $form return $form
->schema([ ->schema([
Select::make('client') Select::make('client')
->default(request()->query('client_id'))
->options(Client::query()->get()->pluck('company', 'id')) ->options(Client::query()->get()->pluck('company', 'id'))
->afterStateUpdated(function ($set, $get) { ->afterStateUpdated(function ($set, $get) {
$set('branch_id', ''); $set('branch_id', '');

View File

@@ -2,10 +2,16 @@
namespace App\Filament\Resources\SaleResource\Pages; namespace App\Filament\Resources\SaleResource\Pages;
use App\Actions\Transactions\CreateTransactionAction;
use App\DataObjects\CreateTransactionDTO;
use App\Filament\Resources\SaleResource; use App\Filament\Resources\SaleResource;
use App\Models\Branch; use App\Models\Branch;
use App\Models\Sale;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Pipeline;
class CreateSale extends CreateRecord class CreateSale extends CreateRecord
{ {
@@ -16,11 +22,48 @@ class CreateSale extends CreateRecord
return $this->getFormDataMutation($data); return $this->getFormDataMutation($data);
} }
protected function handleRecordCreation(array $data): Model
{
$transactions = $this->data['transactions'] ?? [];
return $this->processCreate($data, $transactions);
}
public function getFormDataMutation(array $data): array public function getFormDataMutation(array $data): array
{ {
return Arr::except($data, ['client', 'transactions', 'with_discount']); return Arr::except($data, ['client', 'transactions', 'with_discount']);
} }
public function processCreate(array $data, array $transactions): Model
{
try {
DB::beginTransaction();
$record = Sale::create($data);
$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();
}
DB::commit();
} catch (\Exception $exception) {
DB::rollBack();
throw new \Exception('Failed to save transactions : '.$exception->getMessage());
}
return $record;
}
protected function afterCreate(): void protected function afterCreate(): void
{ {
$branch = Branch::find($this->data['branch_id']); $branch = Branch::find($this->data['branch_id']);

View File

@@ -2,9 +2,15 @@
namespace App\Filament\Resources\SaleResource\Pages; namespace App\Filament\Resources\SaleResource\Pages;
use App\Actions\Transactions\CreateTransactionAction;
use App\DataObjects\CreateTransactionDTO;
use App\Filament\Resources\SaleResource; use App\Filament\Resources\SaleResource;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Pipeline;
class EditSale extends EditRecord class EditSale extends EditRecord
{ {
@@ -16,4 +22,61 @@ class EditSale extends EditRecord
Actions\DeleteAction::make(), Actions\DeleteAction::make(),
]; ];
} }
protected function mutateFormDataBeforeSave(array $data): array
{
return $this->getFormDataMutation($data);
}
protected function handleRecordUpdate(Model $record, array $data): Model
{
$transactions = $this->data['transactions'] ?? [];
return $this->processUpdate($record, $data, $transactions);
}
public function getFormDataMutation(array $data): array
{
return Arr::except($data, ['client', 'transactions', 'with_discount']);
}
public function processUpdate(Model $record, array $data, array $transactions): Model
{
try {
DB::beginTransaction();
$record->update($data);
$branch = $record->branch;
// Delete existing transactions and their related ledgers/balances
$record->transactions->each(function ($transaction) {
$transaction->ledgers->each(function ($ledger) {
$ledger->balances()->delete();
$ledger->delete();
});
$transaction->delete();
});
// Create new transactions
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();
}
DB::commit();
} catch (\Exception $exception) {
DB::rollBack();
throw new \Exception('Failed to save transactions : '.$exception->getMessage());
}
return $record;
}
} }

View File

@@ -73,4 +73,9 @@ class Client extends Model
{ {
return $this->hasManyThrough(Expense::class, Branch::class); return $this->hasManyThrough(Expense::class, Branch::class);
} }
public function journals(): HasManyThrough
{
return $this->hasManyThrough(Journal::class, Branch::class);
}
} }

View File

@@ -25,6 +25,20 @@ class Expense extends Model
return $this->morphMany(Transaction::class, 'transactionable'); return $this->morphMany(Transaction::class, 'transactionable');
} }
protected static function booted()
{
static::deleting(function ($expense) {
$expense->transactions->each(function ($transaction) {
// Delete associated ledgers first to trigger any ledger deletion logic if exists
$transaction->ledgers->each(function ($ledger) {
$ledger->balances()->delete(); // Delete balances associated with ledger
$ledger->delete();
});
$transaction->delete();
});
});
}
public function branch(): BelongsTo public function branch(): BelongsTo
{ {
return $this->belongsTo(Branch::class); return $this->belongsTo(Branch::class);

View File

@@ -25,6 +25,20 @@ class Sale extends Model
return $this->morphMany(Transaction::class, 'transactionable'); return $this->morphMany(Transaction::class, 'transactionable');
} }
protected static function booted()
{
static::deleting(function ($sale) {
$sale->transactions->each(function ($transaction) {
// Delete associated ledgers first to trigger any ledger deletion logic if exists
$transaction->ledgers->each(function ($ledger) {
$ledger->balances()->delete(); // Delete balances associated with ledger
$ledger->delete();
});
$transaction->delete();
});
});
}
public function branch(): BelongsTo public function branch(): BelongsTo
{ {
return $this->belongsTo(Branch::class); return $this->belongsTo(Branch::class);