Browse Source
- Introduced `ChartOfAccount` model with parent-child relationships, casting, and policy integration. - Defined DTOs and actions for registering, adjusting, and suspending accounts (`RegisterChartOfAccount`, `AdjustAccountClassification`, `SuspendAccountPostings`). - Added enums for account classifications and balances. - Created `AccountingServiceProvider` and `RelationshipServiceProvider` for domain initialization and runtime relationship binding. - Migrated COA-related logic from Billing domain and removed redundant files. - Updated DataTables, Livewire forms, and localization for COA and product management.master
62 changed files with 1222 additions and 116 deletions
@ -0,0 +1,19 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Accounting\Actions\LedgerConfiguration; |
|||
|
|||
use App\Domains\Accounting\DTOs\LedgerConfiguration\AdjustAccountClassificationDTO; |
|||
use App\Domains\Accounting\Models\ChartOfAccount; |
|||
|
|||
class AdjustAccountClassification |
|||
{ |
|||
public function execute(ChartOfAccount $coa, AdjustAccountClassificationDTO $dto): void |
|||
{ |
|||
$coa->update([ |
|||
'code' => $dto->code, |
|||
'normal_balance' => $dto->normal_balance, |
|||
'classification' => $dto->classification, |
|||
'parent_id' => $dto->parentId, |
|||
]); |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Accounting\Actions\LedgerConfiguration; |
|||
|
|||
use App\Domains\Accounting\DTOs\LedgerConfiguration\RegisterChartOfAccountDTO; |
|||
use App\Domains\Accounting\Models\ChartOfAccount; |
|||
|
|||
class RegisterChartOfAccount |
|||
{ |
|||
public function execute(RegisterChartOfAccountDTO $dto): void |
|||
{ |
|||
ChartOfAccount::create([ |
|||
'code' => $dto->code, |
|||
'name' => $dto->name, |
|||
'classification' => $dto->classification, |
|||
'normal_balance' => $dto->normal_balance, |
|||
'parent_id' => $dto->parentId, |
|||
]); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Accounting\Actions\LedgerConfiguration; |
|||
|
|||
use App\Domains\Accounting\Models\ChartOfAccount; |
|||
use App\Domains\System\Enums\LifecycleStatus; |
|||
|
|||
class SuspendAccountPostings |
|||
{ |
|||
public function execute(ChartOfAccount $coa): void |
|||
{ |
|||
$coa->update([ |
|||
'status' => LifecycleStatus::INACTIVE, |
|||
]); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Accounting\DTOs\LedgerConfiguration; |
|||
|
|||
use App\Domains\Accounting\Enums\Ledger\AccountBalance; |
|||
use App\Domains\Accounting\Enums\Ledger\AccountClassification; |
|||
|
|||
readonly class AdjustAccountClassificationDTO |
|||
{ |
|||
public function __construct( |
|||
public string $code, |
|||
public string $name, |
|||
public AccountClassification $classification, |
|||
public AccountBalance $normal_balance, |
|||
public ?int $parentId, |
|||
) {} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Accounting\DTOs\LedgerConfiguration; |
|||
|
|||
use App\Domains\Accounting\Enums\Ledger\AccountBalance; |
|||
use App\Domains\Accounting\Enums\Ledger\AccountClassification; |
|||
use App\Domains\System\Enums\LifecycleStatus; |
|||
|
|||
readonly class RegisterChartOfAccountDTO |
|||
{ |
|||
public function __construct( |
|||
public string $code, |
|||
public string $name, |
|||
public AccountClassification $classification, |
|||
public AccountBalance $normal_balance, |
|||
public LifecycleStatus $status, |
|||
public ?int $parentId, |
|||
) {} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Accounting\Enums\Ledger; |
|||
|
|||
use App\Domains\System\Traits\Enum\HasPredicateMethod; |
|||
use App\UI\Enums\Concerns\InteractsWithLabels; |
|||
use App\UI\Enums\Contracts\HasLabel; |
|||
use Yajra\DataTables\Html\HasOptions; |
|||
|
|||
enum AccountBalance: string implements HasLabel |
|||
{ |
|||
use HasPredicateMethod; |
|||
use HasOptions; |
|||
use InteractsWithLabels; |
|||
|
|||
case CREDIT = 'credit'; |
|||
case DEBIT = 'debit'; |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Accounting\Enums\Ledger; |
|||
|
|||
use App\Domains\System\Traits\Enum\HasPredicateMethod; |
|||
use App\UI\Enums\Concerns\InteractsWithLabels; |
|||
use App\UI\Enums\Contracts\HasLabel; |
|||
use Yajra\DataTables\Html\HasOptions; |
|||
|
|||
enum AccountClassification: string implements HasLabel |
|||
{ |
|||
use HasPredicateMethod; |
|||
use HasOptions; |
|||
use InteractsWithLabels; |
|||
|
|||
case LIABILITY = 'liability'; |
|||
case ASSET = 'asset'; |
|||
case EQUITY = 'equity'; |
|||
case REVENUE = 'revenue'; |
|||
case EXPENSE = 'expense'; |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Accounting\Models; |
|||
|
|||
use App\Domains\Accounting\Enums\Ledger\AccountBalance; |
|||
use App\Domains\Accounting\Enums\Ledger\AccountClassification; |
|||
use App\Domains\Accounting\Policies\ChartOfAccountPolicy; |
|||
use App\Domains\System\Traits\Model\HasPublicUlid; |
|||
use Illuminate\Database\Eloquent\Attributes\Fillable; |
|||
use Illuminate\Database\Eloquent\Attributes\UsePolicy; |
|||
use Illuminate\Database\Eloquent\Model; |
|||
use Illuminate\Database\Eloquent\Relations\BelongsTo; |
|||
use Illuminate\Database\Eloquent\Relations\HasMany; |
|||
|
|||
#[Fillable([ |
|||
'code', |
|||
'name', |
|||
'classification', |
|||
'normal_balance', |
|||
'parent_id', |
|||
])] |
|||
#[UsePolicy(ChartOfAccountPolicy::class)] |
|||
class ChartOfAccount extends Model |
|||
{ |
|||
use HasPublicUlid; |
|||
|
|||
protected $casts = [ |
|||
'classification' => AccountClassification::class, |
|||
'normal_balance' => AccountBalance::class, |
|||
]; |
|||
|
|||
public function parent(): BelongsTo |
|||
{ |
|||
return $this->belongsTo(ChartOfAccount::class, 'parent_id'); |
|||
} |
|||
|
|||
public function children(): HasMany |
|||
{ |
|||
return $this->hasMany(ChartOfAccount::class, 'parent_id'); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,34 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Accounting\Policies; |
|||
|
|||
use App\Domains\Accounting\Models\ChartOfAccount; |
|||
use App\Domains\Identity\Models\User; |
|||
|
|||
class ChartOfAccountPolicy |
|||
{ |
|||
public function viewAny(User $user): bool |
|||
{ |
|||
return $user->hasPermissionTo('coa.viewAny'); |
|||
} |
|||
|
|||
public function view(User $user, ChartOfAccount $model): bool |
|||
{ |
|||
return $user->hasPermissionTo('coa.view'); |
|||
} |
|||
|
|||
public function create(User $user): bool |
|||
{ |
|||
return $user->hasPermissionTo('coa.create'); |
|||
} |
|||
|
|||
public function update(User $user, ChartOfAccount $model): bool |
|||
{ |
|||
return $user->hasPermissionTo('coa.update'); |
|||
} |
|||
|
|||
public function delete(User $user, ChartOfAccount $model): bool |
|||
{ |
|||
return $user->hasPermissionTo('coa.delete'); |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Accounting\Providers; |
|||
|
|||
use App\Domains\System\Traits\Provider\RegistersDomainEvents; |
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class AccountingServiceProvider extends ServiceProvider |
|||
{ |
|||
use RegistersDomainEvents; |
|||
|
|||
protected array $listen = []; |
|||
|
|||
public function register(): void |
|||
{ |
|||
$this->app->register(RelationshipServiceProvider::class); |
|||
} |
|||
|
|||
public function boot(): void |
|||
{ |
|||
$this->registerEvents(); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Accounting\Providers; |
|||
|
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class RelationshipServiceProvider extends ServiceProvider |
|||
{ |
|||
/** |
|||
* Boot cross-domain runtime relationships. |
|||
* |
|||
* This provider is strictly dedicated to runtime, decoupled cross-domain |
|||
* relationship definitions. Use Model::resolveRelationUsing() here to wire |
|||
* explicit polymorphic or foreign-key macros between models that live in |
|||
* different domains — without introducing a hard compile-time dependency |
|||
* between those domains. |
|||
* |
|||
* Do NOT add route registrations, bindings, event listeners, or any other |
|||
* bootstrapping logic here. Keep this file focused on relationships only. |
|||
* |
|||
* Example: |
|||
* |
|||
* Order::resolveRelationUsing('customer', fn (Order $order) => |
|||
* $order->belongsTo(Customer::class, 'customer_id') |
|||
* ); |
|||
*/ |
|||
public function boot(): void |
|||
{ |
|||
// Register cross-domain relationships below. |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Billing\Actions\Catalog; |
|||
|
|||
use App\Domains\Billing\DTOs\Catalog\ModifyProductDTO; |
|||
use App\Domains\Billing\Models\Product; |
|||
use Illuminate\Support\Facades\DB; |
|||
|
|||
class ModifyProduct |
|||
{ |
|||
public function execute(Product $product, ModifyProductDTO $dto): Product |
|||
{ |
|||
return DB::transaction(function () use ($product, $dto) { |
|||
$product->update([ |
|||
'name' => $dto->name, |
|||
'code' => $dto->code, |
|||
'coa_id' => $dto->coaId, |
|||
]); |
|||
|
|||
return $product->fresh(); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Billing\Actions\Catalog; |
|||
|
|||
use App\Domains\Billing\DTOs\Catalog\RegisterProductDTO; |
|||
use App\Domains\Billing\Models\Product; |
|||
use Illuminate\Support\Facades\DB; |
|||
|
|||
class RegisterProduct |
|||
{ |
|||
public function execute(RegisterProductDTO $dto): Product |
|||
{ |
|||
return DB::transaction(function () use ($dto) { |
|||
return Product::create([ |
|||
'name' => $dto->name, |
|||
'code' => $dto->code, |
|||
'coa_id' => $dto->coaId, |
|||
]); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Billing\DTOs\Catalog; |
|||
|
|||
readonly class ModifyProductDTO |
|||
{ |
|||
public function __construct( |
|||
public string $name, |
|||
public string $code, |
|||
public int $coaId, |
|||
) {} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Billing\DTOs\Catalog; |
|||
|
|||
readonly class RegisterProductDTO |
|||
{ |
|||
public function __construct( |
|||
public string $name, |
|||
public string $code, |
|||
public int $coaId, |
|||
) {} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Billing\Enums\Catalog; |
|||
|
|||
enum AccountBalance: string |
|||
{ |
|||
case CREDIT = 'credit'; |
|||
case DEBIT = 'debit'; |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Billing\Enums\Catalog; |
|||
|
|||
enum AccountClassification: string |
|||
{ |
|||
case LIABILITY = 'liability'; |
|||
case ASSET = 'asset'; |
|||
case EQUITY = 'equity'; |
|||
case REVENUE = 'revenue'; |
|||
case EXPENSE = 'expense'; |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Billing\Models; |
|||
|
|||
use Database\Factories\Billing\ChartOfAccountFactory; |
|||
use Illuminate\Database\Eloquent\Factories\HasFactory; |
|||
use Illuminate\Database\Eloquent\Attributes\Fillable; |
|||
use Illuminate\Database\Eloquent\Attributes\UseFactory; |
|||
use Illuminate\Database\Eloquent\Model; |
|||
|
|||
#[Fillable([ |
|||
'code', |
|||
'name', |
|||
'classification', |
|||
'normal_balance' |
|||
])] |
|||
#[UseFactory(ChartOfAccountFactory::class)] |
|||
class ChartOfAccount extends Model |
|||
{ |
|||
use HasFactory; |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Payment\Listeners\VirtualAccount; |
|||
|
|||
class InitializeVirtualAccountForPublishedInvoice |
|||
{ |
|||
public function handle(object $event): void |
|||
{ |
|||
// |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers\Web\Accounting; |
|||
|
|||
use App\Attributes\LayoutData; |
|||
use App\Attributes\Seo; |
|||
use App\Http\Controllers\Controller; |
|||
use App\Http\DataTables\Accounting\ChartOfAccountDataTable; |
|||
|
|||
class ChartOfAccountController extends Controller |
|||
{ |
|||
/** |
|||
* Handle the incoming request. |
|||
*/ |
|||
#[Seo( |
|||
title: 'domains/accounting/seo.coa.title', |
|||
description: 'domains/accounting/seo.coa.description', |
|||
keywords: 'domains/accounting/seo.coa.keywords' |
|||
)] |
|||
#[LayoutData( |
|||
header: 'domains/accounting/seo.coa.title', |
|||
breadcrumbs: [ |
|||
'ui/menu.dashboard' => 'dashboard', |
|||
'domains/accounting/seo.coa.title' => '', |
|||
], |
|||
)] |
|||
public function __invoke(ChartOfAccountDataTable $dataTable) |
|||
{ |
|||
return $dataTable->render('pages.accounting.chartofaccounts.index'); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers\Web\Billing; |
|||
|
|||
use App\Attributes\LayoutData; |
|||
use App\Attributes\Seo; |
|||
use App\Http\Controllers\Controller; |
|||
use App\Http\DataTables\Billing\ProductDataTable; |
|||
|
|||
class ProductController extends Controller |
|||
{ |
|||
/** |
|||
* Handle the incoming request. |
|||
*/ |
|||
#[Seo( |
|||
title: 'domains/billing/seo.product.title', |
|||
description: 'domains/billing/seo.product.description', |
|||
keywords: 'domains/billing/seo.product.keywords' |
|||
)] |
|||
#[LayoutData( |
|||
header: 'domains/billing/seo.product.title', |
|||
breadcrumbs: [ |
|||
'ui/menu.dashboard' => 'dashboard', |
|||
'domains/billing/seo.product.title' => '', |
|||
], |
|||
)] |
|||
public function __invoke(ProductDataTable $dataTable) |
|||
{ |
|||
return $dataTable->render('pages.billing.products.index'); |
|||
} |
|||
} |
|||
@ -0,0 +1,130 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\DataTables\Accounting; |
|||
|
|||
use App\Domains\Accounting\Models\ChartOfAccount; |
|||
use Illuminate\Database\Eloquent\Builder as QueryBuilder; |
|||
use Yajra\DataTables\EloquentDataTable; |
|||
use Yajra\DataTables\Html\Builder as HtmlBuilder; |
|||
use Yajra\DataTables\Html\Button; |
|||
use Yajra\DataTables\Html\Column; |
|||
use Yajra\DataTables\Services\DataTable; |
|||
|
|||
class ChartOfAccountDataTable extends DataTable |
|||
{ |
|||
/** |
|||
* Build the DataTable class. |
|||
* |
|||
* @param QueryBuilder<ChartOfAccount> $query Results from query() method. |
|||
*/ |
|||
public function dataTable(QueryBuilder $query): EloquentDataTable |
|||
{ |
|||
return (new EloquentDataTable($query)) |
|||
->editColumn('normal_balance', fn (ChartOfAccount $chartOfAccount) => $chartOfAccount->normal_balance->label()) |
|||
->editColumn('classification', fn (ChartOfAccount $chartOfAccount) => $chartOfAccount->classification->label()) |
|||
->addColumn( |
|||
'action', |
|||
fn (ChartOfAccount $chartOfAccount) => view('components.datatables.action-button', [ |
|||
'edit' => [ |
|||
'modal' => 'chartofaccount-form-modal', |
|||
'permission' => auth()->user()->can('update', $chartOfAccount), |
|||
], |
|||
'delete' => [ |
|||
'url' => null, |
|||
'title' => __('ui/button.delete'), |
|||
'permission' => auth()->user()->can('delete', $chartOfAccount), |
|||
'message' => __('ui/confirmation.delete', ['resource' => __('resources.chartOfAccount')]), |
|||
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.chartOfAccount')]), |
|||
], |
|||
'table_name' => 'chart-of-account-table', |
|||
'id' => $chartOfAccount->ulid, |
|||
]) |
|||
) |
|||
->addIndexColumn(); |
|||
} |
|||
|
|||
/** |
|||
* Get the query source of dataTable. |
|||
* |
|||
* @return QueryBuilder<ChartOfAccount> |
|||
*/ |
|||
public function query(ChartOfAccount $model): QueryBuilder |
|||
{ |
|||
return $model->newQuery(); |
|||
} |
|||
|
|||
/** |
|||
* Optional method if you want to use the html builder. |
|||
*/ |
|||
public function html(): HtmlBuilder |
|||
{ |
|||
return $this->builder() |
|||
->setTableId('chart-of-account-table') |
|||
->columns($this->getColumns()) |
|||
->minifiedAjax() |
|||
->orderBy(1) |
|||
->layout([ |
|||
'topStart' => [ |
|||
'className' => 'col-md-auto me-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-start align-items-center align-items-md-start gap-1', |
|||
'features' => ['buttons', 'pageLength'], |
|||
], |
|||
'topEnd' => [ |
|||
'className' => 'col-md-auto ms-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-end align-items-center align-items-md-start gap-1', |
|||
'features' => ['search'], |
|||
], |
|||
|
|||
'bottomStart' => 'info', |
|||
'bottomEnd' => 'paging', |
|||
]) |
|||
->parameters([ |
|||
'language' => [ |
|||
'search' => '', |
|||
'searchPlaceholder' => __('ui/button.lookup'), |
|||
], |
|||
]) |
|||
->buttons([ |
|||
Button::make('add') |
|||
->action('$("#chartofaccount-form-modal").modal("show");') |
|||
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml()) |
|||
->addClass('btn-sm'), |
|||
Button::make('reload') |
|||
->text(svg('tabler-reload', ['width' => 16, 'height' => 16])->toHtml()) |
|||
->addClass('btn-sm'), |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Get the dataTable columns definition. |
|||
*/ |
|||
public function getColumns(): array |
|||
{ |
|||
return [ |
|||
Column::computed('DT_RowIndex') |
|||
->title('#') |
|||
->searchable(false) |
|||
->orderable(false), |
|||
Column::make('code') |
|||
->title(__('domains/accounting/field.coa.code')), |
|||
Column::make('name') |
|||
->title(__('domains/accounting/field.coa.name')), |
|||
Column::make('normal_balance') |
|||
->title(__('domains/accounting/field.coa.normal_balance')), |
|||
Column::make('classification') |
|||
->title(__('domains/accounting/field.coa.classification')), |
|||
Column::computed('action') |
|||
->title(__('ui/label.actions')) |
|||
->exportable(false) |
|||
->printable(false) |
|||
->width(60) |
|||
->addClass('text-center'), |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Get the filename for export. |
|||
*/ |
|||
protected function filename(): string |
|||
{ |
|||
return 'ChartOfAccountDataTable_' . date('YmdHis'); |
|||
} |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\DataTables\Billing; |
|||
|
|||
use App\Domains\Billing\Models\Product; |
|||
use Illuminate\Database\Eloquent\Builder as QueryBuilder; |
|||
use Yajra\DataTables\EloquentDataTable; |
|||
use Yajra\DataTables\Html\Builder as HtmlBuilder; |
|||
use Yajra\DataTables\Html\Button; |
|||
use Yajra\DataTables\Html\Column; |
|||
use Yajra\DataTables\Services\DataTable; |
|||
|
|||
class ProductDataTable extends DataTable |
|||
{ |
|||
/** |
|||
* Build the DataTable class. |
|||
* |
|||
* @param QueryBuilder<Product> $query Results from query() method. |
|||
*/ |
|||
public function dataTable(QueryBuilder $query): EloquentDataTable |
|||
{ |
|||
return (new EloquentDataTable($query)) |
|||
->editColumn('chart_of_account.name', fn (Product $product) => $product->chartOfAccount?->name ?? '-') |
|||
->addColumn( |
|||
'action', |
|||
fn (Product $product) => view('components.datatables.action-button', [ |
|||
'edit' => [ |
|||
'modal' => 'product-form-modal', |
|||
'permission' => auth()->user()->can('update', $product), |
|||
], |
|||
'delete' => [ |
|||
'url' => null, |
|||
'title' => __('ui/button.delete'), |
|||
'permission' => auth()->user()->can('delete', $product), |
|||
'message' => __('ui/confirmation.delete', ['resource' => __('resources.product')]), |
|||
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.product')]), |
|||
], |
|||
'table_name' => 'product-table', |
|||
'id' => $product->id, |
|||
]) |
|||
) |
|||
->rawColumns(['action']) |
|||
->addIndexColumn(); |
|||
} |
|||
|
|||
/** |
|||
* Get the query source of dataTable. |
|||
* |
|||
* @return QueryBuilder<Product> |
|||
*/ |
|||
public function query(Product $model): QueryBuilder |
|||
{ |
|||
return $model->newQuery()->with(['chartOfAccount', 'pricing']); |
|||
} |
|||
|
|||
/** |
|||
* Optional method if you want to use the html builder. |
|||
*/ |
|||
public function html(): HtmlBuilder |
|||
{ |
|||
return $this->builder() |
|||
->setTableId('product-table') |
|||
->columns($this->getColumns()) |
|||
->minifiedAjax() |
|||
->orderBy(1) |
|||
->layout([ |
|||
'topStart' => [ |
|||
'className' => 'col-md-auto me-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-start align-items-center align-items-md-start gap-1', |
|||
'features' => ['buttons', 'pageLength'], |
|||
], |
|||
'topEnd' => [ |
|||
'className' => 'col-md-auto ms-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-end align-items-center align-items-md-start gap-1', |
|||
'features' => ['search'], |
|||
], |
|||
|
|||
'bottomStart' => 'info', |
|||
'bottomEnd' => 'paging', |
|||
]) |
|||
->parameters([ |
|||
'language' => [ |
|||
'search' => '', |
|||
'searchPlaceholder' => __('ui/button.lookup'), |
|||
], |
|||
'scrollX' => true, |
|||
'scrollCollapse' => true, |
|||
'responsive' => true, |
|||
]) |
|||
->buttons([ |
|||
Button::make('add') |
|||
->action('$("#product-form-modal").modal("show");') |
|||
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml()) |
|||
->addClass('btn-sm'), |
|||
Button::make('reload') |
|||
->text(svg('tabler-reload', ['width' => 16, 'height' => 16])->toHtml()) |
|||
->addClass('btn-sm'), |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Get the dataTable columns definition. |
|||
*/ |
|||
public function getColumns(): array |
|||
{ |
|||
return [ |
|||
Column::computed('DT_RowIndex') |
|||
->title('#') |
|||
->searchable(false) |
|||
->orderable(false), |
|||
Column::make('code') |
|||
->title(__('domains/billing/field.product.code')), |
|||
Column::make('name') |
|||
->title(__('domains/billing/field.product.name')), |
|||
Column::make('chart_of_account.name') |
|||
->title(__('domains/billing/field.product.coa')), |
|||
Column::computed('action') |
|||
->title(__('ui/label.actions')) |
|||
->exportable(false) |
|||
->printable(false) |
|||
->width(60) |
|||
->addClass('text-center'), |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Get the filename for export. |
|||
*/ |
|||
protected function filename(): string |
|||
{ |
|||
return 'ProductDataTable_' . date('YmdHis'); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
<?php |
|||
|
|||
namespace App\Livewire\Forms\Accounting; |
|||
|
|||
use App\Domains\Accounting\Enums\Ledger\AccountBalance; |
|||
use App\Domains\Accounting\Enums\Ledger\AccountClassification; |
|||
use App\Domains\System\Enums\LifecycleStatus; |
|||
use Livewire\Attributes\Validate; |
|||
use Livewire\Form; |
|||
|
|||
class ChartOfAccountForm extends Form |
|||
{ |
|||
#[Validate('required|string|max:255', as: 'domains/accounting/field.coa.code')] |
|||
public string $code = ''; |
|||
|
|||
#[Validate('required|string|max:255', as: 'domains/accounting/field.coa.name')] |
|||
public string $name = ''; |
|||
|
|||
#[Validate('required', as: 'domains/accounting/field.coa.classification')] |
|||
public ?AccountClassification $classification = null; |
|||
|
|||
#[Validate('required', as: 'domains/accounting/field.coa.normal_balance')] |
|||
public ?AccountBalance $normal_balance = null; |
|||
|
|||
#[Validate(as: 'domains/accounting/field.coa.parent')] |
|||
public int|string|null $parent_id = null; |
|||
|
|||
#[Validate('required', as: 'domains/accounting/field.coa.status')] |
|||
public ?LifecycleStatus $status = LifecycleStatus::ACTIVE; |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
<?php |
|||
|
|||
namespace App\Livewire\Forms\Billing; |
|||
|
|||
use Livewire\Attributes\Validate; |
|||
use Livewire\Form; |
|||
|
|||
class ProductForm extends Form |
|||
{ |
|||
#[Validate('required|string|max:255', as: 'domains/billing/field.product.code')] |
|||
public string $code = ''; |
|||
|
|||
#[Validate('required|string|max:255', as: 'domains/billing/field.product.name')] |
|||
public string $name = ''; |
|||
|
|||
#[Validate('required|integer|exists:chart_of_accounts,id', as: 'domains/billing/field.product.coa')] |
|||
public ?int $coa_id = null; |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
<?php |
|||
|
|||
namespace App\Livewire\Forms\Finance; |
|||
|
|||
use App\Domains\Finance\Enums\AccountClassification; |
|||
use App\Domains\System\Enums\LifecycleStatus; |
|||
use Livewire\Attributes\Validate; |
|||
use Livewire\Form; |
|||
|
|||
class ChartOfAccountForm extends Form |
|||
{ |
|||
#[Validate('required|string|max:255', as: 'domains/finance/field.coa.code')] |
|||
public string $code = ''; |
|||
|
|||
#[Validate('required|string|max:255', as: 'domains/finance/field.coa.name')] |
|||
public string $name = ''; |
|||
|
|||
#[Validate('required', as: 'domains/finance/field.coa.classification')] |
|||
public AccountClassification $classification = AccountClassification::REVENUE; |
|||
|
|||
#[Validate(as: 'domains/finance/field.coa.parent')] |
|||
public ?string $parent_id = null; |
|||
|
|||
#[Validate('required', as: 'domains/finance/field.coa.status')] |
|||
public LifecycleStatus $status = LifecycleStatus::ACTIVE; |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
use App\Domains\Accounting\Enums\Ledger\AccountClassification; |
|||
use App\Domains\Accounting\Enums\Ledger\AccountBalance; |
|||
|
|||
return [ |
|||
|
|||
'account_balance' => [ |
|||
AccountBalance::CREDIT->value => 'Credit', |
|||
AccountBalance::DEBIT->value => 'Debit', |
|||
], |
|||
|
|||
|
|||
'account_classification' => [ |
|||
AccountClassification::ASSET->value => 'Asset', |
|||
AccountClassification::LIABILITY->value => 'Liability', |
|||
AccountClassification::EQUITY->value => 'Equity', |
|||
AccountClassification::REVENUE->value => 'Revenue', |
|||
AccountClassification::EXPENSE->value => 'Expense', |
|||
], |
|||
]; |
|||
@ -0,0 +1,12 @@ |
|||
<?php |
|||
|
|||
return [ |
|||
'coa' => [ |
|||
'code' => 'Account Code', |
|||
'name' => 'Account Name', |
|||
'normal_balance' => 'Normal Balance', |
|||
'classification' => 'Classification', |
|||
'status' => 'Status', |
|||
'parent' => 'Derived From', |
|||
], |
|||
]; |
|||
@ -0,0 +1,9 @@ |
|||
<?php |
|||
|
|||
return [ |
|||
'coa' => [ |
|||
'title' => 'Chart of Accounts (COA) Data', |
|||
'description' => 'Manage chart of accounts for financial records.', |
|||
'keywords' => 'chart of accounts, coa, accounting, finance', |
|||
], |
|||
]; |
|||
@ -0,0 +1,9 @@ |
|||
<?php |
|||
|
|||
return [ |
|||
'product' => [ |
|||
'code' => 'Product Code', |
|||
'name' => 'Product Name', |
|||
'coa' => 'Chart of Account (COA)', |
|||
], |
|||
]; |
|||
@ -0,0 +1,9 @@ |
|||
<?php |
|||
|
|||
return [ |
|||
'product' => [ |
|||
'title' => 'Product Data', |
|||
'description' => 'Manage products and their chart of account mappings.', |
|||
'keywords' => 'product, products, billing, finance, coa', |
|||
], |
|||
]; |
|||
@ -0,0 +1,19 @@ |
|||
<?php |
|||
|
|||
use App\Domains\Accounting\Enums\Ledger\AccountBalance; |
|||
use App\Domains\Accounting\Enums\Ledger\AccountClassification; |
|||
|
|||
return [ |
|||
'account_balance' => [ |
|||
AccountBalance::CREDIT->value => 'Kredit', |
|||
AccountBalance::DEBIT->value => 'Debit', |
|||
], |
|||
|
|||
'account_classification' => [ |
|||
AccountClassification::ASSET->value => 'Aset', |
|||
AccountClassification::LIABILITY->value => 'Liabilitas', |
|||
AccountClassification::EQUITY->value => 'Ekuitas', |
|||
AccountClassification::REVENUE->value => 'Pendapatan', |
|||
AccountClassification::EXPENSE->value => 'Beban', |
|||
], |
|||
]; |
|||
@ -0,0 +1,12 @@ |
|||
<?php |
|||
|
|||
return [ |
|||
'coa' => [ |
|||
'code' => 'Kode Akun', |
|||
'name' => 'Nama Akun', |
|||
'normal_balance' => 'Saldo Normal', |
|||
'classification' => 'Klasifikasi', |
|||
'status' => 'Status', |
|||
'parent' => 'Turunan Dari', |
|||
], |
|||
]; |
|||
@ -0,0 +1,9 @@ |
|||
<?php |
|||
|
|||
return [ |
|||
'coa' => [ |
|||
'title' => 'Data Bagan Akun (COA)', |
|||
'description' => 'Kelola bagan akun untuk pencatatan keuangan.', |
|||
'keywords' => 'bagan akun, coa, akuntansi, keuangan', |
|||
], |
|||
]; |
|||
@ -0,0 +1,9 @@ |
|||
<?php |
|||
|
|||
return [ |
|||
'product' => [ |
|||
'code' => 'Kode Produk', |
|||
'name' => 'Nama Produk', |
|||
'coa' => 'Bagan Akun (COA)', |
|||
], |
|||
]; |
|||
@ -0,0 +1,9 @@ |
|||
<?php |
|||
|
|||
return [ |
|||
'product' => [ |
|||
'title' => 'Data Produk', |
|||
'description' => 'Kelola produk dan pemetaan bagan akun.', |
|||
'keywords' => 'produk, billing, tagihan, coa', |
|||
], |
|||
]; |
|||
@ -0,0 +1,8 @@ |
|||
<?php |
|||
|
|||
return [ |
|||
'clients' => [ |
|||
'synced' => 'Klien berhasil disinkronkan.', |
|||
'fail_to_sync' => 'Klien gagal disinkronkan: :error.', |
|||
], |
|||
]; |
|||
@ -0,0 +1,20 @@ |
|||
<x-layouts.app> |
|||
<x-card> {{ $dataTable->table() }} </x-card> |
|||
|
|||
<livewire:pages::system.audit.audit-view-modal |
|||
key-name="ulid" |
|||
:model="\App\Domains\Accounting\Models\ChartOfAccount::class" |
|||
translation="domains/accounting/field.coa." |
|||
/> |
|||
|
|||
<livewire:pages::accounting.chartofaccounts.form-modal /> |
|||
|
|||
@can('chart-of-account.delete') |
|||
<livewire:datatables.delete-action key-name="ulid" :model="\App\Domains\Accounting\Models\ChartOfAccount::class" /> |
|||
@endcan |
|||
|
|||
@push('page-scripts') |
|||
@vite('resources/js/plugin/datatables.js') |
|||
{{ $dataTable->scripts(attributes: ['type' => 'module']) }} |
|||
@endpush |
|||
</x-layouts.app> |
|||
@ -0,0 +1,56 @@ |
|||
@use(App\Domains\Accounting\Enums\Ledger\AccountBalance) |
|||
@use(App\Domains\Accounting\Enums\Ledger\AccountClassification) |
|||
@use(App\Domains\System\Enums\LifecycleStatus) |
|||
<x-modal id="chartofaccount-form-modal" :title="$this->title" wire:submit="save" wire:loading form livewire> |
|||
<div class="d-flex flex-column gap-3"> |
|||
<x-form.select |
|||
name="form.parent" |
|||
:label="__('domains/accounting/field.coa.parent')" |
|||
wire:model.live="form.parent_id" |
|||
:options="$this->chartOfAccounts->pluck('name', 'ulid')" |
|||
/> |
|||
|
|||
<x-form.input |
|||
name="form.code" |
|||
wire:model="form.code" |
|||
:disabled="$mode == 'update'" |
|||
:label="__('domains/accounting/field.coa.code')" |
|||
/> |
|||
|
|||
<x-form.input |
|||
name="form.name" |
|||
wire:model="form.name" |
|||
:disabled="$mode == 'update'" |
|||
:label="__('domains/accounting/field.coa.name')" |
|||
/> |
|||
|
|||
<x-form.select |
|||
name="form.classification" |
|||
:label="__('domains/accounting/field.coa.normal_balance')" |
|||
wire:model="form.normal_balance" |
|||
:disabled="$form->parent_id != null" |
|||
:options="AccountBalance::options()" |
|||
/> |
|||
|
|||
<x-form.select |
|||
name="form.classification" |
|||
:label="__('domains/accounting/field.coa.classification')" |
|||
wire:model="form.classification" |
|||
:disabled="$form->parent_id != null" |
|||
:options="AccountClassification::options()" |
|||
/> |
|||
|
|||
<x-form.select |
|||
name="form.status" |
|||
:label="__('domains/accounting/field.coa.status')" |
|||
wire:model="form.status" |
|||
:disabled="$mode == 'update'" |
|||
:options="LifecycleStatus::options()" |
|||
/> |
|||
</div> |
|||
|
|||
<x-slot:footer> |
|||
<x-button type="button" theme="secondary" :label="__('ui/button.cancel')" data-coreui-dismiss="modal"/> |
|||
<x-button type="submit" theme="primary" :label="__('ui/button.save')"/> |
|||
</x-slot:footer> |
|||
</x-modal> |
|||
@ -0,0 +1,115 @@ |
|||
<?php |
|||
|
|||
use App\Domains\Accounting\Actions\LedgerConfiguration\AdjustAccountClassification; |
|||
use App\Domains\Accounting\Actions\LedgerConfiguration\RegisterChartOfAccount; |
|||
use App\Domains\Accounting\DTOs\LedgerConfiguration\AdjustAccountClassificationDTO; |
|||
use App\Domains\Accounting\DTOs\LedgerConfiguration\RegisterChartOfAccountDTO; |
|||
use App\Domains\Accounting\Models\ChartOfAccount; |
|||
use App\Livewire\Concerns\WithModal; |
|||
use App\Livewire\Concerns\WithToast; |
|||
use App\Livewire\Forms\Accounting\ChartOfAccountForm; |
|||
use Illuminate\Support\Collection; |
|||
use Livewire\Attributes\Computed; |
|||
use Livewire\Attributes\Locked; |
|||
use Livewire\Component; |
|||
|
|||
new class extends Component |
|||
{ |
|||
use WithModal; |
|||
use WithToast; |
|||
|
|||
public ChartOfAccountForm $form; |
|||
|
|||
#[Locked] |
|||
public ?string $id = null; |
|||
|
|||
#[Locked] |
|||
public string $mode = 'create'; |
|||
|
|||
protected string $resourceName = 'chart_of_account'; |
|||
|
|||
public function updating($property, $value): void |
|||
{ |
|||
if ($property === 'form.parent_id') { |
|||
$props = explode('.', $property); |
|||
$props = implode('->', $props); |
|||
$this->{$props} = $value; |
|||
|
|||
$coa = $this->chartOfAccounts->where('ulid', $value)->first(); |
|||
if ($coa) { |
|||
$this->form->code = $coa->code + 1; |
|||
$this->form->classification = $coa->classification; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public function show(int|string $id): void |
|||
{ |
|||
$this->id = $id; |
|||
$this->mode = 'update'; |
|||
$coa = $this->coa->except(['ulid', 'created_at', 'updated_at']); |
|||
$coa['parent_id'] = $this->chartOfAccounts |
|||
->where('id', $coa['parent_id']) |
|||
->first() |
|||
?->ulid; |
|||
$this->form->fill($coa); |
|||
} |
|||
|
|||
#[Computed] |
|||
public function coa(): ChartOfAccount |
|||
{ |
|||
return $this->id ? ChartOfAccount::where('ulid', $this->id)->first() : new ChartOfAccount; |
|||
} |
|||
|
|||
#[Computed] |
|||
public function chartOfAccounts(): Collection |
|||
{ |
|||
return ChartOfAccount::query() |
|||
->select(['id', 'name', 'code', 'ulid', 'classification']) |
|||
->get(); |
|||
} |
|||
|
|||
public function save(RegisterChartOfAccount $register, AdjustAccountClassification $adjust): void |
|||
{ |
|||
$this->form->validate(); |
|||
|
|||
$parentId = null; |
|||
if($this->form->parent_id) { |
|||
$parentId = $this->chartOfAccounts |
|||
->where('ulid', $this->form->parent_id) |
|||
->first() |
|||
->id; |
|||
} |
|||
|
|||
if ($this->mode === 'create') { |
|||
$register->execute(new RegisterChartOfAccountDTO( |
|||
code: $this->form->code, |
|||
name: $this->form->name, |
|||
classification: $this->form->classification, |
|||
normal_balance: $this->form->normal_balance, |
|||
status: $this->form->status, |
|||
parentId: $parentId, |
|||
)); |
|||
$this->success(__('ui/crud.success.created', ['resource' => __('resources.chart_of_account')])); |
|||
} elseif ($this->mode == 'update') { |
|||
$adjust->execute($this->coa, new AdjustAccountClassificationDTO( |
|||
code: $this->form->code, |
|||
name: $this->form->name, |
|||
classification: $this->form->classification, |
|||
normal_balance: $this->form->normal_balance, |
|||
parentId: $parentId, |
|||
)); |
|||
$this->success(__('ui/crud.success.updated', ['resource' => __('resources.chart_of_account')])); |
|||
} |
|||
|
|||
$this->dispatch('hide-chartofaccount-form-modal'); |
|||
$this->js("LaravelDataTables['chart-of-account-table'].ajax.reload(null, false)"); |
|||
} |
|||
|
|||
public function hide(): void |
|||
{ |
|||
$this->form->reset(); |
|||
$this->reset('id', 'mode'); |
|||
$this->form->reset(); |
|||
} |
|||
}; |
|||
@ -0,0 +1,17 @@ |
|||
<x-layouts.app> |
|||
<x-card> |
|||
{{ $dataTable->table() }} |
|||
</x-card> |
|||
|
|||
<livewire:pages::system.audit.audit-view-modal key-name="id" model="\App\Domains\Billing\Models\Product::class" |
|||
translation="domains/billing/field.product."/> |
|||
|
|||
<livewire:pages::billing.products.form-modal /> |
|||
|
|||
<livewire:datatables.delete-action key-name="id" :model="\App\Domains\Billing\Models\Product::class" /> |
|||
|
|||
@push('page-scripts') |
|||
@vite(['resources/js/plugin/datatables.js', 'resources/js/plugin/select2.js']) |
|||
{{ $dataTable->scripts(attributes: ['type' => 'module']) }} |
|||
@endpush |
|||
</x-layouts.app> |
|||
@ -0,0 +1,27 @@ |
|||
<x-modal id="product-form-modal" :title="$this->title" wire:submit="save" wire:loading form livewire> |
|||
<div class="d-flex flex-column gap-3"> |
|||
<x-form.input |
|||
name="form.code" |
|||
:label="__('domains/billing/field.product.code')" |
|||
wire:model="form.code" |
|||
/> |
|||
<x-form.input |
|||
name="form.name" |
|||
:label="__('domains/billing/field.product.name')" |
|||
wire:model="form.name" |
|||
/> |
|||
<x-form.select x-select2="{ |
|||
dropdownParent: $('#product-form-modal') |
|||
}" |
|||
name="form.coa_id" |
|||
:label="__('domains/billing/field.product.coa')" |
|||
wire:model="form.coa_id" |
|||
:options="$this->coaOptions" |
|||
/> |
|||
</div> |
|||
|
|||
<x-slot:footer> |
|||
<x-button type="button" theme="secondary" :label="__('ui/button.cancel')" data-bs-dismiss="modal" /> |
|||
<x-button type="submit" theme="primary" :label="__('ui/button.save')" /> |
|||
</x-slot:footer> |
|||
</x-modal> |
|||
@ -0,0 +1,83 @@ |
|||
<?php |
|||
|
|||
use App\Domains\Accounting\Models\ChartOfAccount; |
|||
use App\Domains\Billing\Actions\Catalog\ModifyProduct; |
|||
use App\Domains\Billing\Actions\Catalog\RegisterProduct; |
|||
use App\Domains\Billing\DTOs\Catalog\ModifyProductDTO; |
|||
use App\Domains\Billing\DTOs\Catalog\RegisterProductDTO; |
|||
use App\Domains\Billing\Models\Product; |
|||
use App\Livewire\Concerns\WithModal; |
|||
use App\Livewire\Concerns\WithToast; |
|||
use App\Livewire\Forms\Billing\ProductForm; |
|||
use Illuminate\Support\Collection; |
|||
use Livewire\Attributes\Computed; |
|||
use Livewire\Attributes\Locked; |
|||
use Livewire\Component; |
|||
|
|||
new class extends Component |
|||
{ |
|||
use WithModal; |
|||
use WithToast; |
|||
|
|||
#[Locked] |
|||
public ?int $id = null; |
|||
|
|||
#[Locked] |
|||
public string $mode = 'create'; |
|||
|
|||
public ProductForm $form; |
|||
|
|||
protected string $resourceName = 'product'; |
|||
|
|||
public function show(int|string $id): void |
|||
{ |
|||
$this->id = $id; |
|||
$this->mode = 'update'; |
|||
|
|||
$product = Product::findOrFail($id); |
|||
$this->form->fill([ |
|||
'code' => $product->code, |
|||
'name' => $product->name, |
|||
'coa_id' => $product->coa_id, |
|||
]); |
|||
} |
|||
|
|||
#[Computed] |
|||
public function coaOptions(): Collection |
|||
{ |
|||
return ChartOfAccount::query() |
|||
->select(['id', 'name', 'code']) |
|||
->orderBy('code') |
|||
->get() |
|||
->mapWithKeys(fn ($coa) => [$coa->id => "{$coa->code} - {$coa->name}"]); |
|||
} |
|||
|
|||
public function save(RegisterProduct $register, ModifyProduct $modify): void |
|||
{ |
|||
$this->form->validate(); |
|||
|
|||
if ($this->mode === 'create') { |
|||
$register->execute(new RegisterProductDTO( |
|||
name: $this->form->name, |
|||
code: $this->form->code, |
|||
coaId: $this->form->coa_id, |
|||
)); |
|||
} elseif ($this->mode === 'update') { |
|||
$product = Product::findOrFail($this->id); |
|||
$modify->execute($product, new ModifyProductDTO( |
|||
name: $this->form->name, |
|||
code: $this->form->code, |
|||
coaId: $this->form->coa_id, |
|||
)); |
|||
} |
|||
|
|||
$this->dispatch('hide-product-form-modal'); |
|||
$this->js("LaravelDataTables['product-table'].ajax.reload(null, false)"); |
|||
} |
|||
|
|||
public function hide(): void |
|||
{ |
|||
$this->form->reset(); |
|||
$this->reset('id', 'mode'); |
|||
} |
|||
}; |
|||
Loading…
Reference in new issue