From 41413c3c2f7c4f61d728fdf81700715bde25cc54 Mon Sep 17 00:00:00 2001 From: dhanabalan Date: Tue, 1 Sep 2026 12:23:20 +0530 Subject: [PATCH 1/5] Added sale order master migration file --- ...084033_create_sale_order_masters_table.php | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 database/migrations/2026_09_01_084033_create_sale_order_masters_table.php diff --git a/database/migrations/2026_09_01_084033_create_sale_order_masters_table.php b/database/migrations/2026_09_01_084033_create_sale_order_masters_table.php new file mode 100644 index 0000000..abedde4 --- /dev/null +++ b/database/migrations/2026_09_01_084033_create_sale_order_masters_table.php @@ -0,0 +1,44 @@ + Date: Tue, 1 Sep 2026 12:23:54 +0530 Subject: [PATCH 2/5] Added sale order master model file --- app/Models/SaleOrderMaster.php | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 app/Models/SaleOrderMaster.php diff --git a/app/Models/SaleOrderMaster.php b/app/Models/SaleOrderMaster.php new file mode 100644 index 0000000..ae67156 --- /dev/null +++ b/app/Models/SaleOrderMaster.php @@ -0,0 +1,32 @@ +belongsTo(Plant::class); + } + + public function item(): BelongsTo + { + return $this->belongsTo(Item::class); + } +} From 369fbb52459ee2936dfcc81f898b14af288c1868 Mon Sep 17 00:00:00 2001 From: dhanabalan Date: Tue, 1 Sep 2026 12:25:09 +0530 Subject: [PATCH 3/5] Added sale order master resource pages --- .../Resources/SaleOrderMasterResource.php | 193 ++++++++++++++++++ .../Pages/CreateSaleOrderMaster.php | 12 ++ .../Pages/EditSaleOrderMaster.php | 22 ++ .../Pages/ListSaleOrderMasters.php | 19 ++ .../Pages/ViewSaleOrderMaster.php | 19 ++ 5 files changed, 265 insertions(+) create mode 100644 app/Filament/Resources/SaleOrderMasterResource.php create mode 100644 app/Filament/Resources/SaleOrderMasterResource/Pages/CreateSaleOrderMaster.php create mode 100644 app/Filament/Resources/SaleOrderMasterResource/Pages/EditSaleOrderMaster.php create mode 100644 app/Filament/Resources/SaleOrderMasterResource/Pages/ListSaleOrderMasters.php create mode 100644 app/Filament/Resources/SaleOrderMasterResource/Pages/ViewSaleOrderMaster.php diff --git a/app/Filament/Resources/SaleOrderMasterResource.php b/app/Filament/Resources/SaleOrderMasterResource.php new file mode 100644 index 0000000..c2dcc41 --- /dev/null +++ b/app/Filament/Resources/SaleOrderMasterResource.php @@ -0,0 +1,193 @@ +schema([ + Forms\Components\Select::make('plant_id') + ->label('Plant') + ->reactive() + ->relationship('plant', 'name') + ->options(function (callable $get) { + $userHas = Filament::auth()->user()->plant_id; + + return ($userHas && strlen($userHas) > 0) ? Plant::where('id', $userHas)->pluck('name', 'id')->toArray() : Plant::orderBy('code')->pluck('name', 'id')->toArray(); + }) + ->required(), + Forms\Components\Select::make('item_id') + ->label('Item Code') + ->reactive() + ->searchable() + ->options(function (callable $get) { + $plantId = $get('plant_id'); + if (empty($plantId)) { + return []; + } + + return Item::where('plant_id', $plantId)->pluck('code', 'id'); + }) + ->required(), + Forms\Components\TextInput::make('sale_order_number') + ->label('Sale Order Number'), + Forms\Components\TextInput::make('supplier_name') + ->label('Supplier Name'), + Forms\Components\TextInput::make('quantity') + ->label('Quantity'), + Forms\Components\Hidden::make('created_by') + ->label('Created By') + ->default(Filament::auth()->user()?->name), + Forms\Components\Hidden::make('updated_by') + ->default(Filament::auth()->user()?->name), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('No.') + ->label('No.') + ->getStateUsing(function ($record, $livewire, $column, $rowLoop) { + $paginator = $livewire->getTableRecords(); + $perPage = method_exists($paginator, 'perPage') ? $paginator->perPage() : 10; + $currentPage = method_exists($paginator, 'currentPage') ? $paginator->currentPage() : 1; + + return ($currentPage - 1) * $perPage + $rowLoop->iteration; + }), + Tables\Columns\TextColumn::make('plant.name') + ->label('Plant') + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('item.code') + ->label('Item Code') + ->searchable() + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('sale_order_number') + ->label('Sale Order Number') + ->searchable() + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('supplier_name') + ->label('Supplier Name') + ->searchable() + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('quantity') + ->label('Quantity') + ->searchable() + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('created_at') + ->label('Created At') + ->dateTime() + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('created_by') + ->label('Created By') + ->searchable() + ->alignCenter() + ->sortable(), + Tables\Columns\TextColumn::make('updated_at') + ->label('Updated At') + ->dateTime() + ->alignCenter() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('updated_by') + ->label('Updated By') + ->searchable() + ->alignCenter() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('deleted_at') + ->label('Deleted At') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + Tables\Filters\TrashedFilter::make(), + ]) + ->actions([ + Tables\Actions\ViewAction::make(), + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\ForceDeleteBulkAction::make(), + Tables\Actions\RestoreBulkAction::make(), + ]), + ]) + ->headerActions([ + ImportAction::make() + ->label('Import Sale Order') + ->color('warning') + ->importer(SaleOrderMasterImporter::class) + ->visible(function () { + return Filament::auth()->user()->can('view import sale order master'); + }), + ExportAction::make() + ->label('Export Sale Order') + ->color('warning') + ->exporter(SaleOrderMasterExporter::class) + ->visible(function () { + return Filament::auth()->user()->can('view export sale order master'); + }), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListSaleOrderMasters::route('/'), + 'create' => Pages\CreateSaleOrderMaster::route('/create'), + 'view' => Pages\ViewSaleOrderMaster::route('/{record}'), + 'edit' => Pages\EditSaleOrderMaster::route('/{record}/edit'), + ]; + } + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Resources/SaleOrderMasterResource/Pages/CreateSaleOrderMaster.php b/app/Filament/Resources/SaleOrderMasterResource/Pages/CreateSaleOrderMaster.php new file mode 100644 index 0000000..2cd2ef3 --- /dev/null +++ b/app/Filament/Resources/SaleOrderMasterResource/Pages/CreateSaleOrderMaster.php @@ -0,0 +1,12 @@ + Date: Tue, 1 Sep 2026 12:26:02 +0530 Subject: [PATCH 4/5] Added sale order master importer and exporter --- .../Exports/SaleOrderMasterExporter.php | 58 +++++++ .../Imports/SaleOrderMasterImporter.php | 153 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 app/Filament/Exports/SaleOrderMasterExporter.php create mode 100644 app/Filament/Imports/SaleOrderMasterImporter.php diff --git a/app/Filament/Exports/SaleOrderMasterExporter.php b/app/Filament/Exports/SaleOrderMasterExporter.php new file mode 100644 index 0000000..0e0f6a7 --- /dev/null +++ b/app/Filament/Exports/SaleOrderMasterExporter.php @@ -0,0 +1,58 @@ +label('NO') + ->state(function ($record) use (&$rowNumber) { + // Increment and return the row number + return ++$rowNumber; + }), + ExportColumn::make('plant.code') + ->label('PLANT CODE'), + ExportColumn::make('item.code') + ->label('ITEM CODE'), + ExportColumn::make('sale_order_number') + ->label('SALE ORDER NUMBER'), + ExportColumn::make('supplier_name') + ->label('SUPPLIER NAME'), + ExportColumn::make('quantity') + ->label('QUANTITY'), + ExportColumn::make('created_at') + ->label('CREATED AT'), + ExportColumn::make('updated_at') + ->label('UPDATED AT'), + ExportColumn::make('created_by') + ->label('CREATED BY'), + ExportColumn::make('updated_by') + ->label('UPDATED BY'), + ExportColumn::make('deleted_at') + ->label('DELETED AT') + ->enabledByDefault(false), + ]; + } + + public static function getCompletedNotificationBody(Export $export): string + { + $body = 'Your sale order master export has completed and ' . number_format($export->successful_rows) . ' ' . str('row')->plural($export->successful_rows) . ' exported.'; + + if ($failedRowsCount = $export->getFailedRowsCount()) { + $body .= ' ' . number_format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to export.'; + } + + return $body; + } +} diff --git a/app/Filament/Imports/SaleOrderMasterImporter.php b/app/Filament/Imports/SaleOrderMasterImporter.php new file mode 100644 index 0000000..b2e1356 --- /dev/null +++ b/app/Filament/Imports/SaleOrderMasterImporter.php @@ -0,0 +1,153 @@ +requiredMapping() + ->exampleHeader('Plant Code') + ->example('1000') + ->label('Plant Code') + ->relationship(resolveUsing: 'code') + ->rules(['required']), + ImportColumn::make('item') + ->requiredMapping() + ->exampleHeader('Item Code') + ->example('630214') + ->label('Item Code') + ->relationship(resolveUsing: 'code') + ->rules(['required']), + ImportColumn::make('sale_order_number') + ->requiredMapping() + ->exampleHeader('Sale Order Number') + ->example('1JA0029512') + ->label('Sale Order Number') + ->rules(['required']), + ImportColumn::make('supplier_name') + ->exampleHeader('Supplier Name') + ->example('ABC Supplier') + ->label('Supplier Name'), + ImportColumn::make('quantity') + ->exampleHeader('Quantity') + ->example('100') + ->label('Quantity'), + ]; + } + + public function resolveRecord(): ?SaleOrderMaster + { + $warnMsg = []; + $plantCod = $this->data['plant']; + $plant = null; + $item = null; + $saleOrder = trim($this->data['sale_order_number']); + $supplierName = trim($this->data['supplier_name']); + + if (Str::length($plantCod) < 4 || ! is_numeric($plantCod) || ! preg_match('/^[1-9]\d{3,}$/', $plantCod)) { + $warnMsg[] = 'Invalid plant code found'; + } else { + $plant = Plant::where('code', $plantCod)->first(); + if (! $plant) { + $warnMsg[] = 'Plant not found'; + } else { + $item = Item::where('code', $this->data['item'])->where('plant_id', $plant->id)->first(); + } + if (! $item) { + $warnMsg[] = 'Item not found'; + } + } + + if (Str::length($this->data['sale_order_number']) < 9) { + $warnMsg[] = 'Invalid Sale Order number found'; + } + + if (empty($this->data['sale_order_number'])) { + $warnMsg[] = 'Sale Order Number cannot be empty.'; + } + + if (empty($this->data['supplier_name'])) { + $warnMsg[] = 'Supplier Name cannot be empty.'; + } + + if (empty($this->data['quantity'])) { + $warnMsg[] = 'Quantity cannot be empty.'; + } + + if (! is_numeric($this->data['quantity'])) { + $warnMsg[] = 'Quantity must be a number.'; + } + + if (isset($this->importedSaleOrders[$saleOrder])) { + + if (strtolower(trim($this->importedSaleOrders[$saleOrder])) != strtolower($supplierName)){ + $warnMsg[] = "Sale Order '{$saleOrder}' has multiple supplier names in the import file."; + } + } + else { + $this->importedSaleOrders[$saleOrder] = $supplierName; + } + + $existingPo = SaleOrderMaster::where('plant_id', $plant->id)->where('sale_order_number', $this->data['sale_order_number'])->first(); + + if ($existingPo && trim(strtolower($existingPo->supplier_name)) != trim(strtolower($this->data['supplier_name']))) + { + $warnMsg[] = "Sale Order '{$this->data['sale_order_number']}' is already mapped to supplier '{$existingPo->supplier_name}'."; + } + + $existingPoItem = SaleOrderMaster::where('plant_id', $plant->id)->where('item_id', $item->id)->where('sale_order_number', $this->data['sale_order_number'])->where('supplier_name', $this->data['supplier_name'])->first(); + + if ($existingPoItem) + { + $warnMsg[] = "Sale Order '{$this->data['sale_order_number']}' is already exist with item code '{$this->data['item']}' and supplier name '{$this->data['supplier_name']}'."; + } + + $user = Filament::auth()->user(); + + $operatorName = $user->name; + + if (! empty($warnMsg)) { + throw new RowImportFailedException(implode(', ', $warnMsg)); + } + + SaleOrderMaster::updateOrCreate([ + 'plant_id' => $plant->id, + 'item_id' => $item->id, + 'sale_order_number' => $this->data['sale_order_number'], + 'supplier_name' => $this->data['supplier_name'], + 'quantity' => $this->data['quantity'] ?? null, + 'created_by' => $operatorName, + 'updated_by' => $operatorName, + ]); + + return null; + } + + public static function getCompletedNotificationBody(Import $import): string + { + $body = 'Your sale order master import has completed and ' . number_format($import->successful_rows) . ' ' . str('row')->plural($import->successful_rows) . ' imported.'; + + if ($failedRowsCount = $import->getFailedRowsCount()) { + $body .= ' ' . number_format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to import.'; + } + + return $body; + } +} From 56d61e54db0b1165c8670a1f3433a9009949742a Mon Sep 17 00:00:00 2001 From: dhanabalan Date: Tue, 1 Sep 2026 12:26:35 +0530 Subject: [PATCH 5/5] Added sale order master policy file --- app/Policies/SaleOrderMasterPolicy.php | 106 +++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 app/Policies/SaleOrderMasterPolicy.php diff --git a/app/Policies/SaleOrderMasterPolicy.php b/app/Policies/SaleOrderMasterPolicy.php new file mode 100644 index 0000000..188d067 --- /dev/null +++ b/app/Policies/SaleOrderMasterPolicy.php @@ -0,0 +1,106 @@ +checkPermissionTo('view-any SaleOrderMaster'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, SaleOrderMaster $saleordermaster): bool + { + return $user->checkPermissionTo('view SaleOrderMaster'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->checkPermissionTo('create SaleOrderMaster'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, SaleOrderMaster $saleordermaster): bool + { + return $user->checkPermissionTo('update SaleOrderMaster'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, SaleOrderMaster $saleordermaster): bool + { + return $user->checkPermissionTo('delete SaleOrderMaster'); + } + + /** + * Determine whether the user can delete any models. + */ + public function deleteAny(User $user): bool + { + return $user->checkPermissionTo('delete-any SaleOrderMaster'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, SaleOrderMaster $saleordermaster): bool + { + return $user->checkPermissionTo('restore SaleOrderMaster'); + } + + /** + * Determine whether the user can restore any models. + */ + public function restoreAny(User $user): bool + { + return $user->checkPermissionTo('restore-any SaleOrderMaster'); + } + + /** + * Determine whether the user can replicate the model. + */ + public function replicate(User $user, SaleOrderMaster $saleordermaster): bool + { + return $user->checkPermissionTo('replicate SaleOrderMaster'); + } + + /** + * Determine whether the user can reorder the models. + */ + public function reorder(User $user): bool + { + return $user->checkPermissionTo('reorder SaleOrderMaster'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, SaleOrderMaster $saleordermaster): bool + { + return $user->checkPermissionTo('force-delete SaleOrderMaster'); + } + + /** + * Determine whether the user can permanently delete any models. + */ + public function forceDeleteAny(User $user): bool + { + return $user->checkPermissionTo('force-delete-any SaleOrderMaster'); + } +}