Compare commits

2 Commits

Author SHA1 Message Date
3cf5f6db6a Merge pull request #6 from kingjaypee12/fix/sales-related-table
feat: add account linking and improve sales table
2026-02-15 18:57:49 +08:00
Jp
f5c8ec04ad 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
2026-02-15 18:57:18 +08:00
10 changed files with 189 additions and 12 deletions

View File

@@ -0,0 +1,29 @@
<?php
namespace App\DataObjects;
use Carbon\Carbon;
readonly class CreateSaleDTO
{
/**
* Create a new class instance.
*/
public function __construct(
protected string $reference_number,
protected Carbon $happened_on,
protected int $branch_id,
protected int $user_id,
)
{}
public function toArray(): array
{
return [
'reference_number' => $this->reference_number,
'happened_on' => $this->happened_on,
'branch_id' => $this->branch_id,
'user_id' => $this->user_id,
];
}
}

View File

@@ -273,6 +273,7 @@ class ExpenseResource extends Resource
Tables\Columns\TextColumn::make('branch.client.company'), Tables\Columns\TextColumn::make('branch.client.company'),
Tables\Columns\TextColumn::make('branch.code'), Tables\Columns\TextColumn::make('branch.code'),
Tables\Columns\TextColumn::make('happened_on'), Tables\Columns\TextColumn::make('happened_on'),
Tables\Columns\TextColumn::make('accounts_list')->label('Accounts'),
]; ];
} }

View File

@@ -58,6 +58,16 @@ class CreateExpense extends CreateRecord
public function getFormDataMutation(array $data): array 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']); return Arr::except($data, ['client', 'transactions']);
} }
@@ -85,6 +95,15 @@ class CreateExpense extends CreateRecord
)->thenReturn(); )->thenReturn();
} }
$accountIds = collect($transactions)
->pluck('account_id')
->filter()
->unique()
->values()
->all();
$this->getRecord()->accounts()->sync($accountIds);
$this->commitDatabaseTransaction(); $this->commitDatabaseTransaction();
} catch (Exception $exception) { } catch (Exception $exception) {
$this->rollBackDatabaseTransaction(); $this->rollBackDatabaseTransaction();

View File

@@ -54,7 +54,7 @@ class SaleResource extends Resource
->live(), ->live(),
TextInput::make('current_series') TextInput::make('current_series')
->label('Series') ->label('Series')
->disabled(), ->readOnly(),
DatePicker::make('happened_on')->label('Date') DatePicker::make('happened_on')->label('Date')
->required() ->required()
->afterStateUpdated(function ($set, $get) { ->afterStateUpdated(function ($set, $get) {
@@ -220,17 +220,10 @@ class SaleResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
TextColumn::make('id')->label('ID')->sortable(), TextColumn::make('branch.code')->label('Branch')->sortable(),
TextColumn::make('client.name')->label('Client')->sortable(), TextColumn::make('reference_number')->label('Reference Number')->sortable(),
TextColumn::make('branch.name')->label('Branch')->sortable(),
TextColumn::make('happened_on')->label('Date')->date()->sortable(), TextColumn::make('happened_on')->label('Date')->date()->sortable(),
TextColumn::make('gross_amount')->label('Gross Amount')->numeric()->sortable(), TextColumn::make('user.name')->label('Created By')->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([ ->filters([
// //

View File

@@ -9,6 +9,7 @@ use App\Filament\Resources\SaleResource;
use App\Models\Branch; use App\Models\Branch;
use App\Models\Client; use App\Models\Client;
use App\Models\Sale; use App\Models\Sale;
use App\Services\Sales\SaleService;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@@ -65,6 +66,16 @@ class CreateSale extends CreateRecord
public function getFormDataMutation(array $data): array 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']); return Arr::except($data, ['client', 'transactions', 'with_discount']);
} }
@@ -72,7 +83,7 @@ class CreateSale extends CreateRecord
{ {
try { try {
DB::beginTransaction(); DB::beginTransaction();
$record = Sale::create($data); $record = app(SaleService::class)->create($this->getFormDataMutation($data));
$branch = $record->branch; $branch = $record->branch;
foreach ($transactions as $transaction) { foreach ($transactions as $transaction) {
@@ -90,6 +101,16 @@ class CreateSale extends CreateRecord
] ]
)->thenReturn(); )->thenReturn();
} }
$accountIds = collect($transactions)
->pluck('account_id')
->filter()
->unique()
->values()
->all();
$record->accounts()->sync($accountIds);
DB::commit(); DB::commit();
} catch (\Exception $exception) { } catch (\Exception $exception) {
DB::rollBack(); DB::rollBack();

View File

@@ -36,6 +36,16 @@ class EditSale extends EditRecord
public function getFormDataMutation(array $data): array 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']); return Arr::except($data, ['client', 'transactions', 'with_discount']);
} }
@@ -71,6 +81,16 @@ class EditSale extends EditRecord
] ]
)->thenReturn(); )->thenReturn();
} }
$accountIds = collect($transactions)
->pluck('account_id')
->filter()
->unique()
->values()
->all();
$record->accounts()->sync($accountIds);
DB::commit(); DB::commit();
} catch (\Exception $exception) { } catch (\Exception $exception) {
DB::rollBack(); DB::rollBack();

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphMany;
class Expense extends Model class Expense extends Model
@@ -43,4 +44,14 @@ class Expense extends Model
{ {
return $this->belongsTo(Branch::class); 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(', ');
}
} }

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphMany;
class Sale extends Model class Sale extends Model
@@ -43,4 +44,19 @@ class Sale extends Model
{ {
return $this->belongsTo(Branch::class); 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);
}
} }

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Services\Sales;
use App\DataObjects\CreateSaleDTO;
use App\Models\Sale;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
class SaleService
{
/**
* Create a new class instance.
*/
public function __construct()
{
//
}
public function create(array $data): Sale
{
$tData = new CreateSaleDTO(
reference_number: $data['current_series'],
happened_on: Carbon::parse($data['happened_on']),
branch_id: $data['branch_id'],
user_id: Auth::user()->id,
);
return Sale::create($tData->toArray());
}
}

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('account_sale', function (Blueprint $table) {
$table->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');
}
};