diff --git a/app/Abstracts/Http/Controller.php b/app/Abstracts/Http/Controller.php index 8d1beb69d..40f7153e6 100644 --- a/app/Abstracts/Http/Controller.php +++ b/app/Abstracts/Http/Controller.php @@ -153,4 +153,54 @@ abstract class Controller extends BaseController } } } + + public function setActiveTabForCategories(): void + { + if (! request()->has('list_records') && ! request()->has('search')) { + $tab_pins = setting('favorites.tab.' . user()->id, []); + $tab_pins = ! empty($tab_pins) ? json_decode($tab_pins, true) : []; + + if (! empty($tab_pins) && ! empty($tab_pins['categories'])) { + $tab = $tab_pins['categories']; + + if (! empty($tab)) { + + request()->offsetSet('list_records', $tab); + request()->offsetSet('programmatic', '1'); + } + } + } + + if (request()->get('list_records') == 'all') { + return; + } + + $types = $this->getSearchStringValue('type'); + + if (!empty($types)) { + $types = is_string($types) ? explode(',', $types) : $types; + + $tab = config('type.category.' . $types[0] . '.group') ? config('type.category.' . $types[0] . '.group') : 'all'; + + if (!empty($types) && count($types) > 0) { + request()->offsetSet('list_records', $tab); + + $currentSearch = request('search', ''); + $searchParts = array_filter(explode(' ', $currentSearch), function($part) { + return !empty(trim($part)) && !str_starts_with(trim($part), 'type:'); + }); + + $searchParts[] = 'type:' . implode(',', $types); + + request()->offsetSet('search', implode(' ', $searchParts)); + request()->offsetSet('programmatic', '1'); + return; + } + } + + if (empty($tab)) { + request()->offsetSet('list_records', 'all'); + request()->offsetSet('programmatic', '1'); + } + } } diff --git a/app/Abstracts/Listeners/Report.php b/app/Abstracts/Listeners/Report.php index f50f0acfc..553c7fd9b 100644 --- a/app/Abstracts/Listeners/Report.php +++ b/app/Abstracts/Listeners/Report.php @@ -5,13 +5,14 @@ namespace App\Abstracts\Listeners; use App\Models\Banking\Account; use App\Models\Common\Contact; use App\Models\Setting\Category; +use App\Traits\Categories; use App\Traits\Contacts; use App\Traits\DateTime; use App\Traits\SearchString; abstract class Report { - use Contacts, DateTime, SearchString; + use Categories, Contacts, DateTime, SearchString; protected $classes = []; @@ -90,22 +91,24 @@ abstract class Report public function getItemCategories($limit = false) { - return $this->getCategories('item', $limit); + return $this->getCategories($this->getItemCategoryTypes(), $limit); } public function getIncomeCategories($limit = false) { - return $this->getCategories('income', $limit); + return $this->getCategories($this->getIncomeCategoryTypes(), $limit); } public function getExpenseCategories($limit = false) { - return $this->getCategories('expense', $limit); + return $this->getCategories($this->getExpenseCategoryTypes(), $limit); } public function getIncomeExpenseCategories($limit = false) { - return $this->getCategories(['income', 'expense'], $limit); + $types = array_merge($this->getIncomeCategoryTypes(), $this->getExpenseCategoryTypes()); + + return $this->getCategories($types, $limit); } public function getCategories($types, $limit = false) diff --git a/app/Exports/Settings/Categories.php b/app/Exports/Settings/Categories.php index c4a486493..fe9fe8af2 100644 --- a/app/Exports/Settings/Categories.php +++ b/app/Exports/Settings/Categories.php @@ -25,9 +25,11 @@ class Categories extends Export public function fields(): array { return [ + 'code', 'name', 'type', 'color', + 'description', 'parent_name', 'enabled', ]; diff --git a/app/Http/Controllers/Modals/Categories.php b/app/Http/Controllers/Modals/Categories.php index 370b4afab..b2ec178aa 100644 --- a/app/Http/Controllers/Modals/Categories.php +++ b/app/Http/Controllers/Modals/Categories.php @@ -6,10 +6,14 @@ use App\Abstracts\Http\Controller; use App\Http\Requests\Setting\Category as Request; use App\Jobs\Setting\CreateCategory; use App\Models\Setting\Category; +use App\Traits\Categories as Helper; +use App\Traits\Modules; use Illuminate\Http\Request as IRequest; class Categories extends Controller { + use Helper, Modules; + /** * Instantiate a new controller instance. */ @@ -29,19 +33,50 @@ class Categories extends Controller */ public function create(IRequest $request) { - $type = $request->get('type', 'item'); + $type = $request->get('type', Category::ITEM_TYPE); + + $type_codes = []; + + switch ($type) { + case Category::INCOME_TYPE: + $types = $this->getIncomeCategoryTypes(); + break; + case Category::EXPENSE_TYPE: + $types = $this->getExpenseCategoryTypes(); + break; + case Category::ITEM_TYPE: + $types = $this->getItemCategoryTypes(); + break; + case Category::OTHER_TYPE: + $types = $this->getOtherCategoryTypes(); + break; + default: + $types = [$type]; + } + + foreach ($types as $type) { + $config_type = config('type.category.' . $type, []); + $type_codes[$type] = empty($config_type['hide']) || ! in_array('code', $config_type['hide']); + } + + $config_type = config('type.category.' . $type, []); + $show_code_field = ! empty($config_type['hide']) && in_array('code', $config_type['hide']) ? false : true; $categories = collect(); - Category::type($type)->enabled()->orderBy('name')->get()->each(function ($category) use (&$categories) { - $categories->push([ - 'id' => $category->id, - 'title' => $category->name, - 'level' => $category->level, - ]); - }); + Category::type($types) + ->enabled() + ->orderBy('name') + ->get() + ->each(function ($category) use (&$categories) { + $categories->push([ + 'id' => $category->id, + 'title' => $category->name, + 'level' => $category->level, + ]); + }); - $html = view('modals.categories.create', compact('type', 'categories'))->render(); + $html = view('modals.categories.create', compact('type', 'types', 'categories', 'show_code_field', 'type_codes'))->render(); return response()->json([ 'success' => true, @@ -61,7 +96,7 @@ class Categories extends Controller public function store(Request $request) { $request['enabled'] = 1; - $request['type'] = $request->get('type', 'income'); + $request['type'] = $request->get('type', Category::ITEM_TYPE); $request['color'] = $request->get('color', '#' . dechex(rand(0x000000, 0xFFFFFF))); $response = $this->ajaxDispatch(new CreateCategory($request)); diff --git a/app/Http/Controllers/Settings/Categories.php b/app/Http/Controllers/Settings/Categories.php index dd71fa3eb..e3e74dbb0 100644 --- a/app/Http/Controllers/Settings/Categories.php +++ b/app/Http/Controllers/Settings/Categories.php @@ -24,17 +24,44 @@ class Categories extends Controller */ public function index() { + $this->setActiveTabForCategories(); + $query = Category::with('sub_categories'); - if (request()->has('search')) { - $query->withSubcategory(); + if (search_string_value('searchable')) { + $query->withSubCategory(); } $types = $this->getCategoryTypes(); - $categories = $query->type(array_keys($types))->collect(); + if (request()->get('list_records') == 'all') { + $query->type(array_keys($types)); + } - return $this->response('settings.categories.index', compact('categories', 'types')); + $categories = $query->collect(); + + $tabs = $this->getCategoryTabs(); + + $tab = request()->get('list_records'); + $tab_active = ! empty($tab) ? 'categories-' . $tab : 'categories-all'; + + $hide_code_column = true; + + $search_string_type = search_string_value('type'); + $selected_types = ! empty($search_string_type) ? explode(',', $search_string_type) : array_keys($types); + + foreach (config('type.category', []) as $type => $config) { + if (! in_array($type, $selected_types)) { + continue; + } + + if (empty($config['hide']) || !in_array('code', $config['hide'])) { + $hide_code_column = false; + break; + } + } + + return $this->response('settings.categories.index', compact('categories', 'types', 'tabs', 'tab_active', 'hide_code_column')); } /** @@ -54,11 +81,13 @@ class Categories extends Controller */ public function create() { - $types = $this->getCategoryTypes(); + $types = $this->getCategoryTypes(true, true); $categories = []; + $type_codes = []; foreach (config('type.category') as $type => $config) { + $type_codes[$type] = empty($config['hide']) || ! in_array('code', $config['hide']); $categories[$type] = []; } @@ -70,7 +99,7 @@ class Categories extends Controller ]; }); - return view('settings.categories.create', compact('types', 'categories')); + return view('settings.categories.create', compact('types', 'categories', 'type_codes')); } /** @@ -134,15 +163,17 @@ class Categories extends Controller */ public function edit(Category $category) { - $types = $this->getCategoryTypes(); + $types = $this->getCategoryTypes(true, true); $type_disabled = (Category::where('type', $category->type)->count() == 1) ?: false; $edited_category_id = $category->id; $categories = []; + $type_codes = []; foreach (config('type.category') as $type => $config) { + $type_codes[$type] = empty($config['hide']) || ! in_array('code', $config['hide']); $categories[$type] = []; } @@ -175,7 +206,7 @@ class Categories extends Controller $parent_categories = $categories[$category->type] ?? []; - return view('settings.categories.edit', compact('category', 'types', 'type_disabled', 'categories', 'parent_categories')); + return view('settings.categories.edit', compact('category', 'types', 'type_disabled', 'categories', 'parent_categories', 'type_codes')); } /** diff --git a/app/Http/Controllers/Settings/Defaults.php b/app/Http/Controllers/Settings/Defaults.php index 2970a72f1..0b1163537 100644 --- a/app/Http/Controllers/Settings/Defaults.php +++ b/app/Http/Controllers/Settings/Defaults.php @@ -6,9 +6,12 @@ use App\Abstracts\Http\SettingController; use App\Models\Banking\Account; use App\Models\Setting\Category; use App\Models\Setting\Tax; +use App\Traits\Categories; class Defaults extends SettingController { + use Categories; + public function edit() { $accounts = Account::enabled()->orderBy('name')->get()->pluck('title', 'id'); @@ -39,11 +42,16 @@ class Defaults extends SettingController $taxes = Tax::enabled()->orderBy('name')->get()->pluck('title', 'id'); + $income_category_types = $this->getIncomeCategoryTypes('string'); + $expense_category_types = $this->getExpenseCategoryTypes('string'); + return view('settings.default.edit', compact( 'accounts', 'sales_categories', 'purchases_categories', 'taxes', + 'income_category_types', + 'expense_category_types', )); } } diff --git a/app/Http/Requests/Setting/Category.php b/app/Http/Requests/Setting/Category.php index cb7c7c89c..428427983 100644 --- a/app/Http/Requests/Setting/Category.php +++ b/app/Http/Requests/Setting/Category.php @@ -15,8 +15,14 @@ class Category extends FormRequest { $types = collect(config('type.category'))->keys(); + $type = $this->request->get('type'); + $config = config('type.category.' . $type, []); + $code_hidden = !empty($config['hide']) && in_array('code', $config['hide']); + $code = $code_hidden ? 'nullable|string' : 'required|string'; + return [ 'name' => 'required|string', + 'code' => $code, 'type' => 'required|string|in:' . $types->implode(','), 'color' => 'required|string|colour', ]; diff --git a/app/Http/Resources/Setting/Category.php b/app/Http/Resources/Setting/Category.php index 22e377313..b0c3ef1d6 100644 --- a/app/Http/Resources/Setting/Category.php +++ b/app/Http/Resources/Setting/Category.php @@ -17,9 +17,11 @@ class Category extends JsonResource return [ 'id' => $this->id, 'company_id' => $this->company_id, + 'code' => $this->code, 'name' => $this->name, 'type' => $this->type, 'color' => $this->color, + 'description' => $this->description, 'enabled' => $this->enabled, 'parent_id' => $this->parent_id, 'created_from' => $this->created_from, diff --git a/app/Imports/Purchases/Bills/Sheets/BillTransactions.php b/app/Imports/Purchases/Bills/Sheets/BillTransactions.php index 979a5b6e6..c962c6c29 100644 --- a/app/Imports/Purchases/Bills/Sheets/BillTransactions.php +++ b/app/Imports/Purchases/Bills/Sheets/BillTransactions.php @@ -5,6 +5,7 @@ namespace App\Imports\Purchases\Bills\Sheets; use App\Abstracts\Import; use App\Http\Requests\Banking\Transaction as Request; use App\Models\Banking\Transaction as Model; +use App\Models\Setting\Category; class BillTransactions extends Import { @@ -36,9 +37,9 @@ class BillTransactions extends Import $row = parent::map($row); - $row['type'] = 'expense'; + $row['type'] = Model::EXPENSE_TYPE; $row['account_id'] = $this->getAccountId($row); - $row['category_id'] = $this->getCategoryId($row, 'expense'); + $row['category_id'] = $this->getCategoryId($row, Category::EXPENSE_TYPE); $row['contact_id'] = $this->getContactId($row, 'vendor'); $row['currency_code'] = $this->getCurrencyCode($row); $row['document_id'] = $this->getDocumentId($row); diff --git a/app/Imports/Sales/Invoices/Sheets/InvoiceTransactions.php b/app/Imports/Sales/Invoices/Sheets/InvoiceTransactions.php index f8ac26af8..98a864ce9 100644 --- a/app/Imports/Sales/Invoices/Sheets/InvoiceTransactions.php +++ b/app/Imports/Sales/Invoices/Sheets/InvoiceTransactions.php @@ -5,6 +5,7 @@ namespace App\Imports\Sales\Invoices\Sheets; use App\Abstracts\Import; use App\Http\Requests\Banking\Transaction as Request; use App\Models\Banking\Transaction as Model; +use App\Models\Setting\Category; class InvoiceTransactions extends Import { @@ -37,10 +38,10 @@ class InvoiceTransactions extends Import $row = parent::map($row); - $row['type'] = 'income'; + $row['type'] = Model::INCOME_TYPE; $row['currency_code'] = $this->getCurrencyCode($row); $row['account_id'] = $this->getAccountId($row); - $row['category_id'] = $this->getCategoryId($row, 'income'); + $row['category_id'] = $this->getCategoryId($row, Category::INCOME_TYPE); $row['contact_id'] = $this->getContactId($row, 'customer'); $row['document_id'] = $this->getDocumentId($row); $row['number'] = $row['transaction_number']; diff --git a/app/Imports/Settings/Categories.php b/app/Imports/Settings/Categories.php index fca5af8fa..928c81ced 100644 --- a/app/Imports/Settings/Categories.php +++ b/app/Imports/Settings/Categories.php @@ -15,6 +15,8 @@ class Categories extends Import public $columns = [ 'name', 'type', + 'code', + 'description', ]; public function model(array $row) diff --git a/app/Listeners/Report/AddExpenseCategories.php b/app/Listeners/Report/AddExpenseCategories.php index 36cec3f63..9685b3465 100644 --- a/app/Listeners/Report/AddExpenseCategories.php +++ b/app/Listeners/Report/AddExpenseCategories.php @@ -27,7 +27,7 @@ class AddExpenseCategories extends Listener // send true for add limit on search and filter.. $event->class->filters['categories'] = $this->getExpenseCategories(true); - $event->class->filters['routes']['categories'] = ['categories.index', 'search=type:expense enabled:1']; + $event->class->filters['routes']['categories'] = ['categories.index', 'search=type:' . $this->getExpenseCategoryTypes('string') . ' enabled:1']; $event->class->filters['multiple']['categories'] = true; } diff --git a/app/Listeners/Report/AddIncomeCategories.php b/app/Listeners/Report/AddIncomeCategories.php index ec7476144..d492a955d 100644 --- a/app/Listeners/Report/AddIncomeCategories.php +++ b/app/Listeners/Report/AddIncomeCategories.php @@ -27,7 +27,7 @@ class AddIncomeCategories extends Listener // send true for add limit on search and filter.. $event->class->filters['categories'] = $this->getIncomeCategories(true); - $event->class->filters['routes']['categories'] = ['categories.index', 'search=type:income enabled:1']; + $event->class->filters['routes']['categories'] = ['categories.index', 'search=type:' . $this->getIncomeCategoryTypes('string') . ' enabled:1']; $event->class->filters['multiple']['categories'] = true; } diff --git a/app/Listeners/Report/AddIncomeExpenseCategories.php b/app/Listeners/Report/AddIncomeExpenseCategories.php index 3f9a681d5..9552e4fd4 100644 --- a/app/Listeners/Report/AddIncomeExpenseCategories.php +++ b/app/Listeners/Report/AddIncomeExpenseCategories.php @@ -28,7 +28,7 @@ class AddIncomeExpenseCategories extends Listener } $event->class->filters['categories'] = $this->getIncomeExpenseCategories(true); - $event->class->filters['routes']['categories'] = ['categories.index', 'search=type:income,expense enabled:1']; + $event->class->filters['routes']['categories'] = ['categories.index', 'search=type:' . implode(',', array_merge($this->getIncomeCategoryTypes(), $this->getExpenseCategoryTypes())) . ' enabled:1']; $event->class->filters['multiple']['categories'] = true; } @@ -69,7 +69,7 @@ class AddIncomeExpenseCategories extends Listener return; } - $categories = Category::type(['income', 'expense'])->orderBy('name')->get(); + $categories = Category::type(array_merge($this->getIncomeCategoryTypes(), $this->getExpenseCategoryTypes()))->orderBy('name')->get(); $rows = $categories->pluck('name', 'id')->toArray(); $this->setRowNamesAndValuesForCategories($event, $rows, $categories); @@ -83,10 +83,12 @@ class AddIncomeExpenseCategories extends Listener { foreach ($event->class->dates as $date) { foreach ($event->class->tables as $table_key => $table_name) { + $table_keys = $table_key == Category::INCOME_TYPE ? $this->getIncomeCategoryTypes() : $this->getExpenseCategoryTypes(); + foreach ($rows as $id => $name) { $category = $categories->where('id', $id)->first(); - if ($category->type != $table_key) { + if (!in_array($category->type, $table_keys)) { continue; } @@ -100,10 +102,12 @@ class AddIncomeExpenseCategories extends Listener public function setTreeNodesForCategories($event, $nodes, $categories) { foreach ($event->class->tables as $table_key => $table_name) { + $table_keys = $table_key == Category::INCOME_TYPE ? $this->getIncomeCategoryTypes() : $this->getExpenseCategoryTypes(); + foreach ($nodes as $id => $node) { $category = $categories->where('id', $id)->first(); - if ($category->type != $table_key) { + if (!in_array($category->type, $table_keys)) { continue; } diff --git a/app/Models/Common/Company.php b/app/Models/Common/Company.php index b526a4345..002d83b3f 100644 --- a/app/Models/Common/Company.php +++ b/app/Models/Common/Company.php @@ -616,9 +616,10 @@ class Company extends Eloquent implements Ownable setting()->forgetAll(); setting()->load(true); - // Override settings and currencies + // Override settings, currencies, and category types Overrider::load('settings'); Overrider::load('currencies'); + Overrider::load('categoryTypes'); event(new CompanyMadeCurrent($this)); diff --git a/app/Models/Setting/Category.php b/app/Models/Setting/Category.php index 239965176..2f68a75ee 100644 --- a/app/Models/Setting/Category.php +++ b/app/Models/Setting/Category.php @@ -4,11 +4,13 @@ namespace App\Models\Setting; use App\Abstracts\Model; use App\Builders\Category as Builder; +use App\Models\Banking\Transaction; use App\Models\Document\Document; use App\Interfaces\Export\WithParentSheet; use App\Relations\HasMany\Category as HasMany; use App\Scopes\Category as Scope; use App\Traits\Categories; +use App\Traits\DateTime; use App\Traits\Tailwind; use App\Traits\Transactions; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; @@ -17,7 +19,7 @@ use Illuminate\Database\Eloquent\Model as EloquentModel; class Category extends Model { - use Categories, HasFactory, Tailwind, Transactions; + use Categories, HasFactory, Tailwind, Transactions, DateTime; public const INCOME_TYPE = 'income'; public const EXPENSE_TYPE = 'expense'; @@ -33,14 +35,14 @@ class Category extends Model * * @var array */ - protected $fillable = ['company_id', 'name', 'type', 'color', 'enabled', 'created_from', 'created_by', 'parent_id']; + protected $fillable = ['company_id', 'code', 'name', 'type', 'color', 'description', 'enabled', 'created_from', 'created_by', 'parent_id']; /** * Sortable columns. * * @var array */ - public $sortable = ['name', 'type', 'enabled']; + public $sortable = ['code', 'name', 'type', 'enabled']; /** * The "booted" method of the model. @@ -137,6 +139,18 @@ class Category extends Model return $this->hasMany('App\Models\Banking\Transaction'); } + /** + * Scope code. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param $code + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeCode($query, $code) + { + return $query->where('code', $code); + } + /** * Scope to only include categories of a given type. * @@ -155,46 +169,50 @@ class Category extends Model /** * Scope to include only income. + * Uses Categories trait to support multiple income types (e.g. from modules). * * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ public function scopeIncome($query) { - return $query->where($this->qualifyColumn('type'), '=', 'income'); + return $query->whereIn($this->qualifyColumn('type'), $this->getIncomeCategoryTypes()); } /** * Scope to include only expense. + * Uses Categories trait to support multiple expense types (e.g. from modules). * * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ public function scopeExpense($query) { - return $query->where($this->qualifyColumn('type'), '=', 'expense'); + return $query->whereIn($this->qualifyColumn('type'), $this->getExpenseCategoryTypes()); } /** * Scope to include only item. + * Uses Categories trait to support multiple item types (e.g. from modules). * * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ public function scopeItem($query) { - return $query->where($this->qualifyColumn('type'), '=', 'item'); + return $query->whereIn($this->qualifyColumn('type'), $this->getItemCategoryTypes()); } /** * Scope to include only other. + * Uses Categories trait to support multiple other types (e.g. from modules). * * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ public function scopeOther($query) { - return $query->where($this->qualifyColumn('type'), '=', 'other'); + return $query->whereIn($this->qualifyColumn('type'), $this->getOtherCategoryTypes()); } public function scopeName($query, $name) @@ -213,6 +231,17 @@ class Category extends Model return $query->withoutGlobalScope(new Scope); } + /** + * Scope gets only parent categories. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIsNotSubCategory($query) + { + return $query->whereNull('parent_id'); + } + /** * Scope to export the rows of the current page filtered and sorted. * @@ -233,7 +262,7 @@ class Category extends Model $search = $request->get('search'); - $query->withSubcategory(); + $query->withSubCategory(); $query->usingSearchString($search)->sortable($sort); @@ -261,9 +290,92 @@ class Category extends Model /** * Get the display name of the category. */ - public function getDisplayNameAttribute() + public function getDisplayNameAttribute(): string { - return $this->name . ' (' . ucfirst($this->type) . ')'; + $typeConfig = config('type.category.' . $this->type, []); + $hideCode = isset($typeConfig['hide']) && in_array('code', $typeConfig['hide']); + + $typeNames = $this->getCategoryTypes(); + $typeName = $typeNames[$this->type] ?? ucfirst($this->type); + + $prefix = (!$hideCode && $this->code) ? $this->code . ' - ' : ''; + + return $prefix . $this->name . ' (' . $typeName . ')'; + } + + /** + * Get the balance of a category. + * + * @return double + */ + public function getBalanceAttribute() + { + // If view composer has set the balance, return it directly + if (isset($this->de_balance)) { + return $this->de_balance; + } + + $financial_year = $this->getFinancialYear(); + + $start_date = $financial_year->getStartDate(); + $end_date = $financial_year->getEndDate(); + + $this->transactions->whereBetween('paid_at', [$start_date, $end_date]) + ->each(function ($transaction) use (&$incomes, &$expenses) { + if (($transaction->isNotIncome() && $transaction->isNotExpense()) || $transaction->isTransferTransaction()) { + return; + } + + if ($transaction->isIncome()) { + $incomes += $transaction->getAmountConvertedToDefault(); + } else { + $expenses += $transaction->getAmountConvertedToDefault(); + } + }); + + $balance = $incomes - $expenses; + + $this->sub_categories() + ->each(function ($sub_category) use (&$balance) { + $balance += $sub_category->balance; + }); + + return $balance; + } + + /** + * Get the balance of a category without considering sub categories. + * + * @return double + */ + public function getBalanceWithoutSubcategoriesAttribute() + { + // If view composer has set the balance, return it directly + if (isset($this->without_subcategory_de_balance)) { + return $this->without_subcategory_de_balance; + } + + $financial_year = $this->getFinancialYear(); + + $start_date = $financial_year->getStartDate(); + $end_date = $financial_year->getEndDate(); + + $this->transactions->whereBetween('paid_at', [$start_date, $end_date]) + ->each(function ($transaction) use (&$incomes, &$expenses) { + if (($transaction->isNotIncome() && $transaction->isNotExpense()) || $transaction->isTransferTransaction()) { + return; + } + + if ($transaction->isIncome()) { + $incomes += $transaction->getAmountConvertedToDefault(); + } else { + $expenses += $transaction->getAmountConvertedToDefault(); + } + }); + + $balance = $incomes - $expenses; + + return $balance; } /** @@ -303,6 +415,19 @@ class Category extends Model return $actions; } + /** + * A no-op callback that gets fired when a model is cloning but before it gets + * committed to the database + * + * @param Illuminate\Database\Eloquent\Model $src + * @param boolean $child + * @return void + */ + public function onCloning($src, $child = null) + { + $this->code = $this->getNextCategoryCode(); + } + /** * Create a new factory instance for the model. * diff --git a/app/Traits/Categories.php b/app/Traits/Categories.php index a965169dc..aca78c98b 100644 --- a/app/Traits/Categories.php +++ b/app/Traits/Categories.php @@ -8,7 +8,97 @@ use Illuminate\Support\Str; trait Categories { - public function getCategoryTypes(bool $translate = true): array + public function isIncomeCategory(): bool + { + $type = $this->type ?? $this->category->type ?? $this->model->type ?? Category::INCOME_TYPE; + + return in_array($type, $this->getIncomeCategoryTypes()); + } + + public function isExpenseCategory(): bool + { + $type = $this->type ?? $this->category->type ?? $this->model->type ?? Category::EXPENSE_TYPE; + + return in_array($type, $this->getExpenseCategoryTypes()); + } + + public function isItemCategory(): bool + { + $type = $this->type ?? $this->category->type ?? $this->model->type ?? Category::ITEM_TYPE; + + return in_array($type, $this->getItemCategoryTypes()); + } + + public function isOtherCategory(): bool + { + $type = $this->type ?? $this->category->type ?? $this->model->type ?? Category::OTHER_TYPE; + + return in_array($type, $this->getOtherCategoryTypes()); + } + + public function getIncomeCategoryTypes(string $return = 'array'): string|array + { + return $this->getCategoryTypesByIndex(Category::INCOME_TYPE, $return); + } + + public function getExpenseCategoryTypes(string $return = 'array'): string|array + { + return $this->getCategoryTypesByIndex(Category::EXPENSE_TYPE, $return); + } + + public function getItemCategoryTypes(string $return = 'array'): string|array + { + return $this->getCategoryTypesByIndex(Category::ITEM_TYPE, $return); + } + + public function getOtherCategoryTypes(string $return = 'array'): string|array + { + return $this->getCategoryTypesByIndex(Category::OTHER_TYPE, $return); + } + + public function getCategoryTypesByIndex(string $index, string $return = 'array'): string|array + { + $types = (string) setting('category.type.' . $index); + + return ($return == 'array') ? explode(',', $types) : $types; + } + + public function addIncomeCategoryType(string $new_type): void + { + $this->addCategoryType($new_type, Category::INCOME_TYPE); + } + + public function addExpenseCategoryType(string $new_type): void + { + $this->addCategoryType($new_type, Category::EXPENSE_TYPE); + } + + public function addItemCategoryType(string $new_type): void + { + $this->addCategoryType($new_type, Category::ITEM_TYPE); + } + + public function addOtherCategoryType(string $new_type): void + { + $this->addCategoryType($new_type, Category::OTHER_TYPE); + } + + public function addCategoryType(string $new_type, string $index): void + { + $types = !empty(setting('category.type.' . $index)) ? explode(',', setting('category.type.' . $index)) : []; + + if (in_array($new_type, $types)) { + return; + } + + $types[] = $new_type; + + setting([ + 'category.type.' . $index => implode(',', $types), + ])->save(); + } + + public function getCategoryTypes(bool $translate = true, bool $group = false): array { $types = []; $configs = config('type.category'); @@ -22,12 +112,48 @@ trait Categories $name = $attr['alias'] . '::' . $name; } - $types[$type] = $translate ? trans_choice($name, 1) : $name; + if ($group) { + $group_key = $attr['group'] ?? $type; + $types[$group_key][$type] = $translate ? trans_choice($name, 1) : $name; + } else { + $types[$type] = $translate ? trans_choice($name, 1) : $name; + } } return $types; } + public function getCategoryTabs(): array + { + $tabs = []; + $configs = config('type.category'); + + foreach ($configs as $type => $attr) { + $tab_key = 'categories-' . ($attr['group'] ?? $type); + + if (isset($tabs[$tab_key])) { + $tabs[$tab_key]['key'] .= ',' . $type; + continue; + } + + $plural_type = Str::plural($attr['group'] ?? $type); + + $name = $attr['translation']['prefix'] . '.' . $plural_type; + + if (!empty($attr['alias'])) { + $name = $attr['alias'] . '::' . $name; + } + + $tabs[$tab_key] = [ + 'key' => $type, + 'name' => trans_choice($name, 2), + 'show_code' => $attr['show_code'] ?? false, + ]; + } + + return $tabs; + } + public function getCategoryWithoutChildren(int $id): mixed { return Category::getWithoutChildren()->find($id); @@ -36,7 +162,7 @@ trait Categories public function getTransferCategoryId(): mixed { // 1 hour set cache for same query - return Cache::remember('transferCategoryId', 60, function () { + return Cache::remember('transferCategoryId.' . company_id(), 60, function () { return Category::other()->pluck('id')->first(); }); } @@ -62,4 +188,16 @@ trait Categories return $ids; } + + /** + * Finds existing maximum code and increase it + * + * @return mixed + */ + public function getNextCategoryCode() + { + return Category::isNotSubCategory()->get(['code'])->reject(function ($category) { + return !preg_match('/^[0-9]*$/', $category->code); + })->max('code') + 1; + } } diff --git a/app/Traits/Import.php b/app/Traits/Import.php index 7db337881..827d1df7f 100644 --- a/app/Traits/Import.php +++ b/app/Traits/Import.php @@ -85,7 +85,7 @@ trait Import { $id = isset($row['category_id']) ? $row['category_id'] : null; - $type = !empty($type) ? $type : (!empty($row['type']) ? $row['type'] : 'income'); + $type = !empty($type) ? $type : (!empty($row['type']) ? $row['type'] : Category::INCOME_TYPE); if (empty($id) && !empty($row['category_name'])) { $id = $this->getCategoryIdFromName($row, $type); @@ -96,14 +96,14 @@ trait Import public function getCategoryType($type) { - return array_key_exists($type, config('type.category')) ? $type : 'other'; + return array_key_exists($type, config('type.category')) ? $type : Category::OTHER_TYPE; } public function getContactId($row, $type = null) { $id = isset($row['contact_id']) ? $row['contact_id'] : null; - $type = !empty($type) ? $type : (!empty($row['type']) ? (($row['type'] == 'income') ? 'customer' : 'vendor') : 'customer'); + $type = !empty($type) ? $type : (!empty($row['type']) ? (($row['type'] == Transaction::INCOME_TYPE) ? 'customer' : 'vendor') : 'customer'); if (empty($row['contact_id']) && !empty($row['contact_email'])) { $id = $this->getContactIdFromEmail($row, $type); @@ -180,7 +180,7 @@ trait Import } if (empty($id) && !empty($row['invoice_bill_number'])) { - if ($row['type'] == 'income') { + if ($row['type'] == Transaction::INCOME_TYPE) { $id = Document::invoice()->number($row['invoice_bill_number'])->pluck('id')->first(); } else { $id = Document::bill()->number($row['invoice_bill_number'])->pluck('id')->first(); diff --git a/app/Traits/SearchString.php b/app/Traits/SearchString.php index 01b01488d..885c25954 100644 --- a/app/Traits/SearchString.php +++ b/app/Traits/SearchString.php @@ -23,6 +23,10 @@ trait SearchString foreach ($columns as $column) { $variable = preg_split('/:|>?=/', $column); + if ($name == 'searchable' && count($variable) == 1 && preg_match('/^".*"$/', $variable[0])) { + return trim($variable[0], '"'); + } + if (empty($variable[0]) || ($variable[0] != $name) || empty($variable[1])) { continue; } diff --git a/app/Traits/ViewComponents.php b/app/Traits/ViewComponents.php index 753161346..64d2d2295 100644 --- a/app/Traits/ViewComponents.php +++ b/app/Traits/ViewComponents.php @@ -4,6 +4,7 @@ namespace App\Traits; use Akaunting\Module\Module; use App\Events\Common\BulkActionsAdding; +use App\Models\Setting\Category; use App\Traits\Modules; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Route; @@ -188,19 +189,19 @@ trait ViewComponents case 'bill': case 'expense': case 'purchase': - $category_type = 'expense'; + $category_type = Category::EXPENSE_TYPE; break; case 'item': - $category_type = 'item'; + $category_type = Category::ITEM_TYPE; break; case 'other': - $category_type = 'other'; + $category_type = Category::OTHER_TYPE; break; case 'transfer': $category_type = 'transfer'; break; default: - $category_type = 'income'; + $category_type = Category::INCOME_TYPE; break; } diff --git a/app/Utilities/Overrider.php b/app/Utilities/Overrider.php index aac21fe94..1ee269994 100644 --- a/app/Utilities/Overrider.php +++ b/app/Utilities/Overrider.php @@ -3,7 +3,9 @@ namespace App\Utilities; use Akaunting\Money\Money; +use App\Models\Setting\Category; use App\Models\Setting\Currency; +use Illuminate\Support\Str; class Overrider { @@ -60,7 +62,7 @@ class Overrider } // Set locale for Money package - Money::setLocale(app()->getLocale()); + Money::setLocale(app()->getLocale()); // Money config(['money.defaults.currency' => setting('default.currency')]); @@ -71,6 +73,45 @@ class Overrider } } + protected static function loadCategoryTypes() + { + $category = new Category; + + $income_types = $category->getIncomeCategoryTypes('string'); + $expense_types = $category->getExpenseCategoryTypes('string'); + $item_types = $category->getItemCategoryTypes('string'); + $other_types = $category->getOtherCategoryTypes('string'); + + $search_string = config('search-string'); + + foreach ($search_string as $model => &$model_config) { + $route = $model_config['columns']['category_id']['route'] ?? null; + + // Only update category_id routes that point to categories.index + if (!is_array($route) || ($route[0] ?? '') !== 'categories.index' || !isset($route[1])) { + continue; + } + + // Longest match first (income,expense must come before income) + $replacements = [ + 'type:' . Category::INCOME_TYPE . ',' . Category::EXPENSE_TYPE => 'type:' . $income_types . ',' . $expense_types, + 'type:' . Category::INCOME_TYPE => 'type:' . $income_types, + 'type:' . Category::EXPENSE_TYPE => 'type:' . $expense_types, + 'type:' . Category::ITEM_TYPE => 'type:' . $item_types, + 'type:' . Category::OTHER_TYPE => 'type:' . $other_types, + ]; + + foreach ($replacements as $search => $replace) { + if (Str::contains($route[1], $search)) { + $model_config['columns']['category_id']['route'][1] = Str::replace($search, $replace, $route[1]); + break; + } + } + } + + config(['search-string' => $search_string]); + } + protected static function loadCurrencies() { $currencies = Currency::all(); diff --git a/app/View/Components/Form/Group/Category.php b/app/View/Components/Form/Group/Category.php index c9e1e7db1..e6abde7e0 100644 --- a/app/View/Components/Form/Group/Category.php +++ b/app/View/Components/Form/Group/Category.php @@ -4,10 +4,15 @@ namespace App\View\Components\Form\Group; use App\Abstracts\View\Components\Form; use App\Models\Setting\Category as Model; +use App\Traits\Categories; +use App\Traits\Modules; +use Illuminate\Support\Arr; class Category extends Form { - public $type = 'income'; + use Categories, Modules; + + public $type = Model::INCOME_TYPE; public $path; @@ -15,6 +20,14 @@ class Category extends Form public $categories; + /** @var bool */ + public $group; + + public $option_field = [ + 'key' => 'id', + 'value' => 'name', + ]; + /** * Get the view / contents that represent the component. * @@ -26,26 +39,133 @@ class Category extends Form $this->name = 'category_id'; } - $this->path = route('modals.categories.create', ['type' => $this->type]); - $this->remoteAction = route('categories.index', ['search' => 'type:' . $this->type . ' enabled:1']); + switch ($this->type) { + case Model::INCOME_TYPE: + $types = $this->getIncomeCategoryTypes(); + break; + case Model::EXPENSE_TYPE: + $types = $this->getExpenseCategoryTypes(); + break; + case Model::ITEM_TYPE: + $types = $this->getItemCategoryTypes(); + break; + case Model::OTHER_TYPE: + $types = $this->getOtherCategoryTypes(); + break; + default: + $types = [$this->type]; + } - $this->categories = Model::type($this->type)->enabled()->orderBy('name')->take(setting('default.select_limit'))->get(); + $this->path = route('modals.categories.create', ['type' => $this->type]); + $this->remoteAction = route('categories.index', ['search' => 'type:' . implode(',', $types) . ' enabled:1']); + + $typeLabels = collect($this->getCategoryTypes())->only($types)->all(); + + $is_code = false; + + foreach (config('type.category', []) as $type => $config) { + if (! in_array($type, $types)) { + continue; + } + + if (empty($config['hide']) || ! in_array('code', $config['hide'])) { + $is_code = true; + $this->group = true; + break; + } + } + + $order_by = $is_code ? 'code' : 'name'; + + if ($this->group) { + $this->option_field = [ + 'key' => 'id', + 'value' => 'title', + ]; + } + + $query = Model::type($types); + + $query->enabled() + ->orderBy($order_by); + + if (! $this->group) { + $query->take(setting('default.select_limit')); + } + + $this->categories = $query->get(); + + if ($this->group) { + $groups = []; + + foreach ($this->categories as $category) { + $group = $typeLabels[$category->type] ?? trans_choice('general.others', 1); + + $category->title = ($category->code ? $category->code . ' - ' : '') . $category->name; + $category->group = $group; + + $groups[$group][$category->id] = $category; + } + + ksort($groups); + + $this->categories = $groups; + } $model = $this->getParentData('model'); + $selected_category = null; + + $categoryExists = function ($categoryId): bool { + if (! $this->group) { + return $this->categories->contains(function ($category) use ($categoryId) { + return (int) $category->id === (int) $categoryId; + }); + } + + foreach ($this->categories as $group_categories) { + foreach ($group_categories as $category) { + if ((int) $category->id === (int) $categoryId) { + return true; + } + } + } + + return false; + }; + + $appendCategory = function ($category) use ($typeLabels): void { + if (empty($category)) { + return; + } + + $category->title = ($category->code ? $category->code . ' - ' : '') . $category->name; + + if (! $this->group) { + $this->categories->push($category); + + return; + } + + $group = $typeLabels[$category->type] ?? trans_choice('general.others', 1); + + if (! isset($this->categories[$group])) { + $this->categories[$group] = []; + } + + $this->categories[$group][$category->id] = $category; + + ksort($this->categories); + }; $category_id = old('category.id', old('category_id', null)); if (! empty($category_id)) { $this->selected = $category_id; - $has_category = $this->categories->search(function ($category, int $key) use ($category_id) { - return $category->id === $category_id; - }); - - if ($has_category === false) { + if (! $categoryExists($category_id)) { $category = Model::find($category_id); - $this->categories->push($category); + $appendCategory($category); } } @@ -61,15 +181,15 @@ class Category extends Form $selected_category = Model::find($this->selected); } + if (empty($selected_category) && ! empty($this->selected)) { + $selected_category = Model::find($this->selected); + } + if (! empty($selected_category)) { $selected_category_id = $selected_category->id; - $has_selected_category = $this->categories->search(function ($category, int $key) use ($selected_category_id) { - return $category->id === $selected_category_id; - }); - - if ($has_selected_category === false) { - $this->categories->push($selected_category); + if (! $categoryExists($selected_category_id)) { + $appendCategory($selected_category); } } diff --git a/app/View/Components/Index/Balance.php b/app/View/Components/Index/Balance.php new file mode 100644 index 000000000..28c513e22 --- /dev/null +++ b/app/View/Components/Index/Balance.php @@ -0,0 +1,71 @@ +amount = $this->getAmount($amount); + $this->textColor = $this->getTextColor($amount); + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\Contracts\View\View|string + */ + public function render() + { + return view('components.index.balance'); + } + + /** + * Formats the amount according to the location context. + * + * @param float $amount + * @return string + */ + protected function getAmount($amount) + { + return money($amount, setting('default.currency'), true); + } + + /** + * Gets the class of color considering given amount. + * + * @param float $amount + * @return string|null + */ + protected function getTextColor($amount) + { + switch ($amount) { + case $amount > 0: + return 'text-green'; + case $amount < 0: + return 'text-red'; + default: + return ''; + } + } +} diff --git a/config/search-string.php b/config/search-string.php index 75945eb85..d717c5fc8 100644 --- a/config/search-string.php +++ b/config/search-string.php @@ -1,5 +1,7 @@ [ - 'route' => ['categories.index', 'search=type:income,expense enabled:1'], + 'route' => ['categories.index', 'search=type:' . Category::INCOME_TYPE . ',' . Category::EXPENSE_TYPE . ' enabled:1'], 'fields' => [ 'key' => 'id', 'value' => 'display_name', @@ -246,7 +248,7 @@ return [ 'description' => ['searchable' => true], 'enabled' => ['boolean' => true], 'category_id' => [ - 'route' => ['categories.index', 'search=type:item enabled:1'], + 'route' => ['categories.index', 'search=type:' . Category::ITEM_TYPE . ' enabled:1'], 'fields' => [ 'key' => 'id', 'value' => 'name', @@ -352,7 +354,7 @@ return [ 'contact_phone' => ['searchable' => true], 'contact_address' => ['searchable' => true], 'category_id' => [ - 'route' => ['categories.index', 'search=type:income,expense enabled:1'], + 'route' => ['categories.index', 'search=type:' . Category::INCOME_TYPE . ',' . Category::EXPENSE_TYPE . ' enabled:1'], 'multiple' => true, ], 'parent_id', @@ -403,7 +405,7 @@ return [ 'contact_phone' => ['searchable' => true], 'contact_address' => ['searchable' => true], 'category_id' => [ - 'route' => ['categories.index', 'search=type:expense enabled:1'], + 'route' => ['categories.index', 'search=type:' . Category::EXPENSE_TYPE . ' enabled:1'], 'fields' => [ 'key' => 'id', 'value' => 'name', @@ -459,7 +461,7 @@ return [ 'contact_phone' => ['searchable' => true], 'contact_address' => ['searchable' => true], 'category_id' => [ - 'route' => ['categories.index', 'search=type:income enabled:1'], + 'route' => ['categories.index', 'search=type:' . Category::INCOME_TYPE . ' enabled:1'], 'fields' => [ 'key' => 'id', 'value' => 'name', @@ -480,6 +482,8 @@ return [ App\Models\Setting\Category::class => [ 'columns' => [ 'id', + 'code' => ['searchable' => true], + 'description' => ['searchable' => true], 'name' => ['searchable' => true], 'enabled' => ['boolean' => true], 'type' => [ diff --git a/config/setting.php b/config/setting.php index cb7c480a9..4e79fa0d2 100644 --- a/config/setting.php +++ b/config/setting.php @@ -2,10 +2,11 @@ use App\Models\Common\Contact; use App\Models\Banking\Transaction; +use App\Models\Setting\Category; return [ - /* + /* |-------------------------------------------------------------------------- | Enable / Disable auto save |-------------------------------------------------------------------------- @@ -13,7 +14,7 @@ return [ | Auto-save every time the application shuts down | */ - 'auto_save' => env('SETTING_AUTO_SAVE', false), + 'auto_save' => env('SETTING_AUTO_SAVE', false), /* |-------------------------------------------------------------------------- @@ -31,7 +32,7 @@ return [ 'auto_clear' => env('SETTING_CACHE_AUTO_CLEAR', true), ], - /* + /* |-------------------------------------------------------------------------- | Setting driver |-------------------------------------------------------------------------- @@ -41,9 +42,9 @@ return [ | Supported: "database", "json" | */ - 'driver' => env('SETTING_DRIVER', 'database'), + 'driver' => env('SETTING_DRIVER', 'database'), - /* + /* |-------------------------------------------------------------------------- | Database driver |-------------------------------------------------------------------------- @@ -52,14 +53,14 @@ return [ | the default connection. Set the table and column names. | */ - 'database' => [ - 'connection' => env('SETTING_DATABASE_CONNECTION', null), - 'table' => env('SETTING_DATABASE_TABLE', 'settings'), - 'key' => env('SETTING_DATABASE_KEY', 'key'), - 'value' => env('SETTING_DATABASE_VALUE', 'value'), - ], + 'database' => [ + 'connection' => env('SETTING_DATABASE_CONNECTION', null), + 'table' => env('SETTING_DATABASE_TABLE', 'settings'), + 'key' => env('SETTING_DATABASE_KEY', 'key'), + 'value' => env('SETTING_DATABASE_VALUE', 'value'), + ], - /* + /* |-------------------------------------------------------------------------- | JSON driver |-------------------------------------------------------------------------- @@ -67,11 +68,11 @@ return [ | Options for json driver. Enter the full path to the .json file. | */ - 'json' => [ - 'path' => env('SETTING_JSON_PATH', storage_path('settings.json')), - ], + 'json' => [ + 'path' => env('SETTING_JSON_PATH', storage_path('settings.json')), + ], - /* + /* |-------------------------------------------------------------------------- | Override application config values |-------------------------------------------------------------------------- @@ -83,9 +84,9 @@ return [ | "app.locale" => "settings.locale", | */ - 'override' => [ + 'override' => [ - ], + ], /* |-------------------------------------------------------------------------- @@ -168,6 +169,14 @@ return [ 'bill_days' => env('SETTING_FALLBACK_SCHEDULE_BILL_DAYS', '10,5,3,1'), 'time' => env('SETTING_FALLBACK_SCHEDULE_TIME', '09:00'), ], + 'category' => [ + 'type' => [ + 'income' => env('SETTING_FALLBACK_CATEGORY_TYPE_INCOME', Category::INCOME_TYPE), + 'expense' => env('SETTING_FALLBACK_CATEGORY_TYPE_EXPENSE', Category::EXPENSE_TYPE), + 'item' => env('SETTING_FALLBACK_CATEGORY_TYPE_ITEM', Category::ITEM_TYPE), + 'other' => env('SETTING_FALLBACK_CATEGORY_TYPE_OTHER', Category::OTHER_TYPE), + ], + ], 'contact' => [ 'type' => [ 'customer' => env('SETTING_FALLBACK_CONTACT_TYPE_CUSTOMER', Contact::CUSTOMER_TYPE), diff --git a/config/type.php b/config/type.php index c77f0217b..91b1bd7ca 100644 --- a/config/type.php +++ b/config/type.php @@ -10,31 +10,39 @@ return [ // Categories 'category' => [ Category::INCOME_TYPE => [ - 'alias' => '', + 'alias' => '', + 'group' => Category::INCOME_TYPE, 'translation' => [ 'prefix' => 'general', ], + 'hide' => ['code'], ], Category::EXPENSE_TYPE => [ - 'alias' => '', + 'alias' => '', + 'group' => Category::EXPENSE_TYPE, 'translation' => [ 'prefix' => 'general', ], + 'hide' => ['code'], ], Category::ITEM_TYPE => [ - 'alias' => '', + 'alias' => '', + 'group' => Category::ITEM_TYPE, 'translation' => [ 'prefix' => 'general', ], + 'hide' => ['code'], ], Category::OTHER_TYPE => [ - 'alias' => '', + 'alias' => '', + 'group' => Category::OTHER_TYPE, 'translation' => [ 'prefix' => 'general', ], + 'hide' => ['code'], ], ], @@ -58,9 +66,9 @@ return [ 'section_billing_description' => 'customers.form_description.billing', 'section_address_description' => 'customers.form_description.address', ], - 'category_type' => 'income', - 'document_type' => 'invoice', - 'transaction_type' => 'income', + 'category_type' => Category::INCOME_TYPE, + 'document_type' => Document::INVOICE_TYPE, + 'transaction_type' => Transaction::INCOME_TYPE, 'hide' => [], 'class' => [], 'script' => [ @@ -87,9 +95,9 @@ return [ 'section_billing_description' => 'vendors.form_description.billing', 'section_address_description' => 'vendors.form_description.address', ], - 'category_type' => 'expense', - 'document_type' => 'bill', - 'transaction_type' => 'expense', + 'category_type' => Category::EXPENSE_TYPE, + 'document_type' => Document::BILL_TYPE, + 'transaction_type' => Transaction::EXPENSE_TYPE, 'hide' => [], 'class' => [], 'script' => [ @@ -130,9 +138,9 @@ return [ 'setting' => [ 'prefix' => 'invoice', ], - 'category_type' => 'income', - 'transaction_type' => 'income', - 'contact_type' => 'customer', // use contact type + 'category_type' => Category::INCOME_TYPE, + 'transaction_type' => Transaction::INCOME_TYPE, + 'contact_type' => Contact::CUSTOMER_TYPE, // use contact type 'inventory_stock_action' => 'decrease', // decrease stock in stock tracking 'transaction' => [ 'email_template' => 'invoice_payment_customer', // use email template @@ -185,9 +193,9 @@ return [ 'setting' => [ 'prefix' => 'invoice', ], - 'category_type' => 'income', - 'transaction_type' => 'income', - 'contact_type' => 'customer', // use contact type + 'category_type' => Category::INCOME_TYPE, + 'transaction_type' => Transaction::INCOME_TYPE, + 'contact_type' => Contact::CUSTOMER_TYPE, // use contact type 'inventory_stock_action' => 'decrease', // decrease stock in stock tracking 'hide' => [], // for document items 'class' => [], @@ -235,9 +243,9 @@ return [ 'setting' => [ 'prefix' => 'bill', ], - 'category_type' => 'expense', - 'transaction_type' => 'expense', - 'contact_type' => 'vendor', + 'category_type' => Category::EXPENSE_TYPE, + 'transaction_type' => Transaction::EXPENSE_TYPE, + 'contact_type' => Contact::VENDOR_TYPE, 'inventory_stock_action' => 'increase', // increases stock in stock tracking 'transaction' => [ 'email_template' => 'invoice_payment_customer', // use email template @@ -288,9 +296,9 @@ return [ 'setting' => [ 'prefix' => 'bill', ], - 'category_type' => 'expense', - 'transaction_type' => 'expense', - 'contact_type' => 'vendor', + 'category_type' => Category::EXPENSE_TYPE, + 'transaction_type' => Transaction::EXPENSE_TYPE, + 'contact_type' => Contact::VENDOR_TYPE, 'inventory_stock_action' => 'increase', // increases stock in stock tracking 'hide' => [], 'class' => [], @@ -334,8 +342,9 @@ return [ 'related_document_amount' => 'invoices.invoice_amount', 'transactions' => 'general.incomes', ], - 'contact_type' => 'customer', - 'document_type' => 'invoice', + 'category_type' => Category::INCOME_TYPE, + 'contact_type' => Contact::CUSTOMER_TYPE, + 'document_type' => Document::INVOICE_TYPE, 'split_type' => Transaction::INCOME_SPLIT_TYPE, 'email_template' => 'payment_received_customer', 'script' => [ @@ -343,7 +352,7 @@ return [ 'file' => 'transactions', ], ], - + Transaction::INCOME_TYPE => [ 'group' => 'banking', 'route' => [ @@ -365,8 +374,9 @@ return [ 'related_document_amount' => 'invoices.invoice_amount', 'transactions' => 'general.incomes', ], - 'contact_type' => 'customer', - 'document_type' => 'invoice', + 'category_type' => Category::INCOME_TYPE, + 'contact_type' => Contact::CUSTOMER_TYPE, + 'document_type' => Document::INVOICE_TYPE, 'split_type' => Transaction::INCOME_SPLIT_TYPE, 'email_template' => 'payment_received_customer', 'script' => [ @@ -396,8 +406,9 @@ return [ 'related_document_amount' => 'invoices.invoice_amount', 'transactions' => 'general.incomes', ], - 'contact_type' => 'customer', - 'document_type' => 'invoice', + 'category_type' => Category::INCOME_TYPE, + 'contact_type' => Contact::CUSTOMER_TYPE, + 'document_type' => Document::INVOICE_TYPE, 'split_type' => Transaction::INCOME_SPLIT_TYPE, 'email_template' => 'payment_received_customer', 'script' => [ @@ -427,8 +438,9 @@ return [ 'related_document_amount' => 'invoices.invoice_amount', 'transactions' => 'general.incomes', ], - 'contact_type' => 'customer', - 'document_type' => 'invoice', + 'category_type' => Category::INCOME_TYPE, + 'contact_type' => Contact::CUSTOMER_TYPE, + 'document_type' => Document::INVOICE_TYPE, 'email_template' => 'payment_received_customer', 'script' => [ 'folder' => 'banking', @@ -489,8 +501,9 @@ return [ 'prefix' => 'transactions', // this translation file name. 'related_document_amount' => 'bills.bill_amount', ], - 'contact_type' => 'vendor', - 'document_type' => 'bill', + 'category_type' => Category::EXPENSE_TYPE, + 'contact_type' => Contact::VENDOR_TYPE, + 'document_type' => Document::BILL_TYPE, 'split_type' => Transaction::EXPENSE_SPLIT_TYPE, 'email_template' => 'payment_made_vendor', 'script' => [ @@ -519,8 +532,9 @@ return [ 'prefix' => 'transactions', // this translation file name. 'related_document_amount' => 'bills.bill_amount', ], - 'contact_type' => 'vendor', - 'document_type' => 'bill', + 'category_type' => Category::EXPENSE_TYPE, + 'contact_type' => Contact::VENDOR_TYPE, + 'document_type' => Document::BILL_TYPE, 'split_type' => Transaction::EXPENSE_SPLIT_TYPE, 'email_template' => 'payment_made_vendor', 'script' => [ @@ -549,8 +563,9 @@ return [ 'prefix' => 'transactions', // this translation file name. 'related_document_amount' => 'bills.bill_amount', ], - 'contact_type' => 'vendor', - 'document_type' => 'bill', + 'category_type' => Category::EXPENSE_TYPE, + 'contact_type' => Contact::VENDOR_TYPE, + 'document_type' => Document::BILL_TYPE, 'email_template' => 'payment_made_vendor', 'script' => [ 'folder' => 'banking', diff --git a/database/migrations/2026_02_17_000000_core_v3122.php b/database/migrations/2026_02_17_000000_core_v3122.php new file mode 100644 index 000000000..2f2a2744b --- /dev/null +++ b/database/migrations/2026_02_17_000000_core_v3122.php @@ -0,0 +1,34 @@ +string('code')->nullable()->after('company_id'); + $table->text('description')->nullable()->after('color'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + Schema::table('categories', function (Blueprint $table) { + $table->dropColumn('code'); + $table->dropColumn('description'); + }); + } +}; diff --git a/resources/assets/js/components/AkauntingSelectRemote.vue b/resources/assets/js/components/AkauntingSelectRemote.vue index 0be791ebd..56316d2bd 100644 --- a/resources/assets/js/components/AkauntingSelectRemote.vue +++ b/resources/assets/js/components/AkauntingSelectRemote.vue @@ -20,6 +20,7 @@ :collapse-tags="collapse" :remote-method="remoteMethod" :loading="loading" + :class="[{ 'with-color-prefix': selectedOptionColor, 'with-icon-prefix': icon }]" >
@@ -185,21 +197,25 @@