From f5c8ec04adbf91e953ad4f0d246b0b7a8c43c228 Mon Sep 17 00:00:00 2001 From: Jp Date: Sun, 15 Feb 2026 18:57:18 +0800 Subject: [PATCH] feat: add account linking and improve sales table - Add many-to-many relationships between Sale/Expense and Account models - Create pivot tables for account_sale and account_expense with migrations - Implement account syncing during sale/expense creation and editing - Add accounts_list attribute to display comma-separated account names - Introduce SaleService with DTO for sale creation logic - Simplify sales table columns to show branch, reference, date, and creator - Calculate and store aggregated financial fields from transactions - Make series field read-only instead of disabled in sale form --- app/DataObjects/CreateSaleDTO.php | 29 +++++++++++++++ app/Filament/Resources/ExpenseResource.php | 1 + .../ExpenseResource/Pages/CreateExpense.php | 19 ++++++++++ app/Filament/Resources/SaleResource.php | 15 +++----- .../SaleResource/Pages/CreateSale.php | 23 +++++++++++- .../Resources/SaleResource/Pages/EditSale.php | 20 +++++++++++ app/Models/Expense.php | 11 ++++++ app/Models/Sale.php | 16 +++++++++ app/Services/Sales/SaleService.php | 31 ++++++++++++++++ ...ccount_sale_and_account_expense_tables.php | 36 +++++++++++++++++++ 10 files changed, 189 insertions(+), 12 deletions(-) create mode 100644 app/DataObjects/CreateSaleDTO.php create mode 100644 app/Services/Sales/SaleService.php create mode 100644 database/migrations/2026_02_15_000002_create_account_sale_and_account_expense_tables.php diff --git a/app/DataObjects/CreateSaleDTO.php b/app/DataObjects/CreateSaleDTO.php new file mode 100644 index 0000000..735e1fe --- /dev/null +++ b/app/DataObjects/CreateSaleDTO.php @@ -0,0 +1,29 @@ + $this->reference_number, + 'happened_on' => $this->happened_on, + 'branch_id' => $this->branch_id, + 'user_id' => $this->user_id, + ]; + } +} diff --git a/app/Filament/Resources/ExpenseResource.php b/app/Filament/Resources/ExpenseResource.php index 741a620..a3ab5e9 100644 --- a/app/Filament/Resources/ExpenseResource.php +++ b/app/Filament/Resources/ExpenseResource.php @@ -273,6 +273,7 @@ class ExpenseResource extends Resource Tables\Columns\TextColumn::make('branch.client.company'), Tables\Columns\TextColumn::make('branch.code'), Tables\Columns\TextColumn::make('happened_on'), + Tables\Columns\TextColumn::make('accounts_list')->label('Accounts'), ]; } diff --git a/app/Filament/Resources/ExpenseResource/Pages/CreateExpense.php b/app/Filament/Resources/ExpenseResource/Pages/CreateExpense.php index d4c98ea..8e0acc6 100644 --- a/app/Filament/Resources/ExpenseResource/Pages/CreateExpense.php +++ b/app/Filament/Resources/ExpenseResource/Pages/CreateExpense.php @@ -58,6 +58,16 @@ class CreateExpense extends CreateRecord public function getFormDataMutation(array $data): array { + $transactions = $data['transactions'] ?? []; + + $data['gross_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['gross_amount'] ?? 0)); + $data['exempt'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['exempt'] ?? 0)); + $data['zero_rated'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['zero_rated'] ?? 0)); + $data['vatable_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['vatable_amount'] ?? 0)); + $data['input_tax'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['input_tax'] ?? 0)); + $data['payable_withholding_tax'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['payable_withholding_tax'] ?? 0)); + $data['net_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['net_amount'] ?? 0)); + return Arr::except($data, ['client', 'transactions']); } @@ -85,6 +95,15 @@ class CreateExpense extends CreateRecord )->thenReturn(); } + $accountIds = collect($transactions) + ->pluck('account_id') + ->filter() + ->unique() + ->values() + ->all(); + + $this->getRecord()->accounts()->sync($accountIds); + $this->commitDatabaseTransaction(); } catch (Exception $exception) { $this->rollBackDatabaseTransaction(); diff --git a/app/Filament/Resources/SaleResource.php b/app/Filament/Resources/SaleResource.php index ac9efde..4ba7f7f 100644 --- a/app/Filament/Resources/SaleResource.php +++ b/app/Filament/Resources/SaleResource.php @@ -54,7 +54,7 @@ class SaleResource extends Resource ->live(), TextInput::make('current_series') ->label('Series') - ->disabled(), + ->readOnly(), DatePicker::make('happened_on')->label('Date') ->required() ->afterStateUpdated(function ($set, $get) { @@ -220,17 +220,10 @@ 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('branch.code')->label('Branch')->sortable(), + TextColumn::make('reference_number')->label('Reference Number')->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(), + TextColumn::make('user.name')->label('Created By')->sortable(), ]) ->filters([ // diff --git a/app/Filament/Resources/SaleResource/Pages/CreateSale.php b/app/Filament/Resources/SaleResource/Pages/CreateSale.php index be47ad9..2278109 100644 --- a/app/Filament/Resources/SaleResource/Pages/CreateSale.php +++ b/app/Filament/Resources/SaleResource/Pages/CreateSale.php @@ -9,6 +9,7 @@ use App\Filament\Resources\SaleResource; use App\Models\Branch; use App\Models\Client; use App\Models\Sale; +use App\Services\Sales\SaleService; use Filament\Resources\Pages\CreateRecord; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; @@ -65,6 +66,16 @@ class CreateSale extends CreateRecord public function getFormDataMutation(array $data): array { + $transactions = $data['transactions'] ?? []; + + $data['gross_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['gross_amount'] ?? 0)); + $data['exempt'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['exempt'] ?? 0)); + $data['vatable_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['vatable_amount'] ?? 0)); + $data['output_tax'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['output_tax'] ?? 0)); + $data['payable_withholding_tax'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['payable_withholding_tax'] ?? 0)); + $data['discount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['discount'] ?? 0)); + $data['net_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['net_amount'] ?? 0)); + return Arr::except($data, ['client', 'transactions', 'with_discount']); } @@ -72,7 +83,7 @@ class CreateSale extends CreateRecord { try { DB::beginTransaction(); - $record = Sale::create($data); + $record = app(SaleService::class)->create($this->getFormDataMutation($data)); $branch = $record->branch; foreach ($transactions as $transaction) { @@ -90,6 +101,16 @@ class CreateSale extends CreateRecord ] )->thenReturn(); } + + $accountIds = collect($transactions) + ->pluck('account_id') + ->filter() + ->unique() + ->values() + ->all(); + + $record->accounts()->sync($accountIds); + DB::commit(); } catch (\Exception $exception) { DB::rollBack(); diff --git a/app/Filament/Resources/SaleResource/Pages/EditSale.php b/app/Filament/Resources/SaleResource/Pages/EditSale.php index e5b59ff..c56cf9a 100644 --- a/app/Filament/Resources/SaleResource/Pages/EditSale.php +++ b/app/Filament/Resources/SaleResource/Pages/EditSale.php @@ -36,6 +36,16 @@ class EditSale extends EditRecord public function getFormDataMutation(array $data): array { + $transactions = $data['transactions'] ?? []; + + $data['gross_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['gross_amount'] ?? 0)); + $data['exempt'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['exempt'] ?? 0)); + $data['vatable_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['vatable_amount'] ?? 0)); + $data['output_tax'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['output_tax'] ?? 0)); + $data['payable_withholding_tax'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['payable_withholding_tax'] ?? 0)); + $data['discount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['discount'] ?? 0)); + $data['net_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['net_amount'] ?? 0)); + return Arr::except($data, ['client', 'transactions', 'with_discount']); } @@ -71,6 +81,16 @@ class EditSale extends EditRecord ] )->thenReturn(); } + + $accountIds = collect($transactions) + ->pluck('account_id') + ->filter() + ->unique() + ->values() + ->all(); + + $record->accounts()->sync($accountIds); + DB::commit(); } catch (\Exception $exception) { DB::rollBack(); diff --git a/app/Models/Expense.php b/app/Models/Expense.php index 248a04b..6f6d025 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\MorphMany; class Expense extends Model @@ -43,4 +44,14 @@ class Expense extends Model { return $this->belongsTo(Branch::class); } + + public function accounts(): BelongsToMany + { + return $this->belongsToMany(Account::class, 'account_expense')->withTimestamps(); + } + + public function getAccountsListAttribute(): string + { + return $this->accounts->pluck('account')->implode(', '); + } } diff --git a/app/Models/Sale.php b/app/Models/Sale.php index bf59284..b83bed0 100644 --- a/app/Models/Sale.php +++ b/app/Models/Sale.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\MorphMany; class Sale extends Model @@ -43,4 +44,19 @@ class Sale extends Model { return $this->belongsTo(Branch::class); } + + public function accounts(): BelongsToMany + { + return $this->belongsToMany(Account::class, 'account_sale')->withTimestamps(); + } + + public function getAccountsListAttribute(): string + { + return $this->accounts->pluck('account')->implode(', '); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } } diff --git a/app/Services/Sales/SaleService.php b/app/Services/Sales/SaleService.php new file mode 100644 index 0000000..0f51c61 --- /dev/null +++ b/app/Services/Sales/SaleService.php @@ -0,0 +1,31 @@ +id, + ); + return Sale::create($tData->toArray()); + } +} diff --git a/database/migrations/2026_02_15_000002_create_account_sale_and_account_expense_tables.php b/database/migrations/2026_02_15_000002_create_account_sale_and_account_expense_tables.php new file mode 100644 index 0000000..a820f96 --- /dev/null +++ b/database/migrations/2026_02_15_000002_create_account_sale_and_account_expense_tables.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('sale_id')->constrained()->onDelete('cascade'); + $table->foreignId('account_id')->constrained()->onDelete('cascade'); + $table->timestamps(); + + $table->unique(['sale_id', 'account_id']); + }); + + Schema::create('account_expense', function (Blueprint $table) { + $table->id(); + $table->foreignId('expense_id')->constrained()->onDelete('cascade'); + $table->foreignId('account_id')->constrained()->onDelete('cascade'); + $table->timestamps(); + + $table->unique(['expense_id', 'account_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('account_expense'); + Schema::dropIfExists('account_sale'); + } +}; +