Compare commits
102 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
9d6cfba94f | |
|
|
cabef574d7 | |
|
|
b81befc343 | |
|
|
729834a0ef | |
|
|
a776c7b5a9 | |
|
|
ce25e05b64 | |
|
|
31df25305d | |
|
|
09b4e1641e | |
|
|
3ad75c4d6b | |
|
|
5772eca363 | |
|
|
c82b4883a7 | |
|
|
aba76b12c8 | |
|
|
311054bad7 | |
|
|
9ddab69242 | |
|
|
38677e65c2 | |
|
|
91f1956dc9 | |
|
|
1789662440 | |
|
|
6f50a4e685 | |
|
|
e6c67ef504 | |
|
|
f9c57bd402 | |
|
|
3c033af0d6 | |
|
|
3a94319805 | |
|
|
09574e0335 | |
|
|
804cb8e568 | |
|
|
6df965c930 | |
|
|
2c4d52b15e | |
|
|
2c12e7524c | |
|
|
d85c1b51bb | |
|
|
952626f52a | |
|
|
939d2b726a | |
|
|
8359383ba9 | |
|
|
9aed0937ab | |
|
|
afb52a52f8 | |
|
|
9da40446e6 | |
|
|
a44d9d156b | |
|
|
2b098e5e51 | |
|
|
072e070d09 | |
|
|
752e670a06 | |
|
|
24f0dbb961 | |
|
|
b5d096c615 | |
|
|
12ec20f8ac | |
|
|
e44fd37bad | |
|
|
4829c7728d | |
|
|
fb9f7eaaa3 | |
|
|
7c130a4ece | |
|
|
7f1fc69330 | |
|
|
695f8cf6ae | |
|
|
4a0419774b | |
|
|
c5d61d8f4e | |
|
|
d4f2b5b4da | |
|
|
6c67e99a1b | |
|
|
ef56e7e91e | |
|
|
a7917f42b3 | |
|
|
24fc846ce3 | |
|
|
11459b5bb3 | |
|
|
1f8d3f3cbf | |
|
|
421cc1091a | |
|
|
aa393c58fc | |
|
|
b12cebd08a | |
|
|
4d59bdf754 | |
|
|
df83efb84e | |
|
|
e523979ad6 | |
|
|
d6c9083b80 | |
|
|
c78fb4ce34 | |
|
|
e8556ee306 | |
|
|
86b987eb9b | |
|
|
6cfd9410d6 | |
|
|
0c7fa0cf0d | |
|
|
7e5a64262c | |
|
|
c6716e7763 | |
|
|
b56a3bda56 | |
|
|
89b600dec9 | |
|
|
b5d8499ac7 | |
|
|
1f68dd4b4d | |
|
|
1a0bf56c63 | |
|
|
c7b70dd224 | |
|
|
e76c84e46c | |
|
|
dee95e412e | |
|
|
ac58f4d0ae | |
|
|
c1dd91238d | |
|
|
416f879353 | |
|
|
f720e22117 | |
|
|
8fb34dc8d2 | |
|
|
dc13c8fabb | |
|
|
1bd3c5a153 | |
|
|
70a5e5acdd | |
|
|
47dd774286 | |
|
|
355cee0cf2 | |
|
|
774f64a051 | |
|
|
48ae7a5a8a | |
|
|
6c485567ca | |
|
|
502c7b0d31 | |
|
|
51d2097cd6 | |
|
|
b7492d3ab0 | |
|
|
7317d1b512 | |
|
|
f9b3d9ebfa | |
|
|
2beba59bb1 | |
|
|
c1f73ae6dc | |
|
|
bbf83478ea | |
|
|
795baf9585 | |
|
|
65705deafb | |
|
|
a4f48d2342 |
|
|
@ -16,6 +16,9 @@ use Illuminate\Pagination\Paginator;
|
|||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
/**
|
||||
* @property string $type
|
||||
*/
|
||||
abstract class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests, Jobs, Permissions, Relationships, SearchString, ValidatesRequests;
|
||||
|
|
@ -114,6 +117,19 @@ abstract class Controller extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
if (! request()->has('list_records') && request()->has('search')) {
|
||||
$status = $this->getSearchStringValue('status');
|
||||
|
||||
if (empty($status)) {
|
||||
$tab_pins = setting('favorites.tab.' . user()->id, []);
|
||||
$tab_pins = ! empty($tab_pins) ? json_decode($tab_pins, true) : [];
|
||||
|
||||
if (! empty($tab_pins) && (($tab_pins[$this->type] ?? null) === 'all')) {
|
||||
request()->offsetSet('list_records', 'all');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (request()->get('list_records') == 'all') {
|
||||
return;
|
||||
}
|
||||
|
|
@ -152,5 +168,105 @@ abstract class Controller extends BaseController
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! request()->has('list_records') && request()->has('search')) {
|
||||
$type = $this->getSearchStringValue('type');
|
||||
|
||||
if (empty($type)) {
|
||||
$tab_pins = setting('favorites.tab.' . user()->id, []);
|
||||
$tab_pins = ! empty($tab_pins) ? json_decode($tab_pins, true) : [];
|
||||
|
||||
if (! empty($tab_pins) && (($tab_pins['transactions'] ?? null) === 'all')) {
|
||||
request()->offsetSet('list_records', 'all');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (request()->get('list_records') == 'all') {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = $this->getSearchStringValue('type');
|
||||
|
||||
if (empty($type)) {
|
||||
$search = config('type.transaction.transactions.route.params.income.search');
|
||||
|
||||
request()->offsetSet('search', $search);
|
||||
request()->offsetSet('programmatic', '1');
|
||||
} else {
|
||||
$income = str_replace('type:', 'income', config('type.transaction.transactions.route.params.income.search'));
|
||||
$expense = str_replace('type:', 'expense', config('type.transaction.transactions.route.params.expense.search'));
|
||||
|
||||
if (($type == $income) || ($type == $expense)) {
|
||||
return;
|
||||
}
|
||||
|
||||
request()->offsetSet('list_records', 'all');
|
||||
}
|
||||
}
|
||||
|
||||
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()->has('list_records') && request()->has('search')) {
|
||||
$type = $this->getSearchStringValue('type');
|
||||
|
||||
if (empty($type)) {
|
||||
$tab_pins = setting('favorites.tab.' . user()->id, []);
|
||||
$tab_pins = ! empty($tab_pins) ? json_decode($tab_pins, true) : [];
|
||||
|
||||
if (! empty($tab_pins) && (($tab_pins['categories'] ?? null) === 'all')) {
|
||||
request()->offsetSet('list_records', 'all');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,7 +122,18 @@ abstract class Import implements HasLocalePreference, ShouldQueue, SkipsEmptyRow
|
|||
} catch (ValidationException $e) {
|
||||
foreach ($e->validator->failed() as $attribute => $value) {
|
||||
foreach ($value as $rule => $params) {
|
||||
$validator->addFailure($row . '.' . $attribute, $rule, $params);
|
||||
if ($rule === 'In' && !empty($params)) {
|
||||
$actual_value = $data[$attribute] ?? 'null';
|
||||
$expected_values = implode(', ', $params);
|
||||
$custom_message = trans('validation.in_detailed', [
|
||||
'attribute' => $attribute,
|
||||
'value' => $actual_value,
|
||||
'values' => $expected_values
|
||||
]);
|
||||
$validator->errors()->add($row . '.' . $attribute, $custom_message);
|
||||
} else {
|
||||
$validator->addFailure($row . '.' . $attribute, $rule, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -546,11 +546,19 @@ abstract class Report
|
|||
|
||||
public function divArithmeticAmount(&$current, $amount)
|
||||
{
|
||||
if ($amount == 0) {
|
||||
throw new \InvalidArgumentException('Division by zero is not allowed');
|
||||
}
|
||||
|
||||
$current = $current / $amount;
|
||||
}
|
||||
|
||||
public function modArithmeticAmount(&$current, $amount)
|
||||
{
|
||||
if ($amount == 0) {
|
||||
throw new \InvalidArgumentException('Modulo by zero is not allowed');
|
||||
}
|
||||
|
||||
$current = $current % $amount;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ abstract class Index extends Component
|
|||
public $page;
|
||||
|
||||
public $permissionCreate;
|
||||
|
||||
public $permissionUpdate;
|
||||
|
||||
public $permissionDelete;
|
||||
/* -- Main End -- */
|
||||
|
||||
/* -- Buttons Start -- */
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@ abstract class Index extends Component
|
|||
'text' => trans('general.title.new', ['type' => trans_choice($this->textPage ?? 'general.' . $prefix, 1)]),
|
||||
'description' => trans('general.empty.actions.new', ['type' => strtolower(trans_choice($this->textPage ?? 'general.' . $prefix, 1))]),
|
||||
'active_badge' => true,
|
||||
'stack' => 'create_button',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -494,6 +495,7 @@ abstract class Index extends Component
|
|||
'url' => route($this->importRoute, $this->importRouteParameters),
|
||||
'text' => trans('import.title', ['type' => trans_choice($this->textPage ?? 'general.' . $prefix, 2)]),
|
||||
'description' => trans('general.empty.actions.import', ['type' => strtolower(trans_choice($this->textPage ?? 'general.' . $prefix, 2))]),
|
||||
'stack' => 'import_button',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ abstract class Widget
|
|||
'header' => 'components.widgets.header',
|
||||
];
|
||||
|
||||
public array $data = [];
|
||||
|
||||
public function __construct($model = null)
|
||||
{
|
||||
$this->model = $model;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class Categories extends BulkAction
|
|||
public function edit($request)
|
||||
{
|
||||
$selected = $this->getSelectedInput($request);
|
||||
|
||||
$types = $this->getCategoryTypes();
|
||||
|
||||
return $this->response('bulk-actions.settings.categories.edit', compact('selected', 'types'));
|
||||
|
|
|
|||
|
|
@ -116,9 +116,9 @@ class Handler extends ExceptionHandler
|
|||
*/
|
||||
protected function unauthenticated($request, AuthenticationException $exception)
|
||||
{
|
||||
// Store the current url in the session
|
||||
if ($request->url() !== config('app.url')) {
|
||||
session(['url.intended' => $request->url()]);
|
||||
// Store the current url in the session (fullUrl includes query string)
|
||||
if ($request->fullUrl() !== config('app.url')) {
|
||||
session(['url.intended' => $request->fullUrl()]);
|
||||
}
|
||||
|
||||
return $request->expectsJson()
|
||||
|
|
|
|||
|
|
@ -25,9 +25,11 @@ class Categories extends Export
|
|||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
'code',
|
||||
'name',
|
||||
'type',
|
||||
'color',
|
||||
'description',
|
||||
'parent_name',
|
||||
'enabled',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ class Transactions extends ApiController
|
|||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
if ($request->has('document_id')) {
|
||||
return $this->errorBadRequest(trans('transactions.messages.create_document_transaction_error'));
|
||||
}
|
||||
|
||||
$transaction = $this->dispatch(new CreateTransaction($request));
|
||||
|
||||
return $this->created(route('api.transactions.show', $transaction->id), new Resource($transaction));
|
||||
|
|
@ -57,6 +61,10 @@ class Transactions extends ApiController
|
|||
*/
|
||||
public function update(Transaction $transaction, Request $request)
|
||||
{
|
||||
if ($request->has('document_id')) {
|
||||
return $this->errorBadRequest(trans('transactions.messages.update_document_transaction_error'));
|
||||
}
|
||||
|
||||
$transaction = $this->dispatch(new UpdateTransaction($transaction, $request));
|
||||
|
||||
return new Resource($transaction->fresh());
|
||||
|
|
@ -70,6 +78,10 @@ class Transactions extends ApiController
|
|||
*/
|
||||
public function destroy(Transaction $transaction)
|
||||
{
|
||||
if ($transaction->document_id) {
|
||||
return $this->errorBadRequest(trans('transactions.messages.delete_document_transaction_error'));
|
||||
}
|
||||
|
||||
try {
|
||||
$this->dispatch(new DeleteTransaction($transaction));
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use App\Abstracts\Http\ApiController;
|
|||
use App\Http\Requests\Banking\Transaction as Request;
|
||||
use App\Http\Resources\Banking\Transaction as Resource;
|
||||
use App\Jobs\Banking\CreateBankingDocumentTransaction;
|
||||
use App\Jobs\Banking\UpdateBankingDocumentTransaction;
|
||||
use App\Jobs\Banking\DeleteTransaction;
|
||||
use App\Models\Banking\Transaction;
|
||||
use App\Models\Document\Document;
|
||||
|
|
@ -32,7 +33,7 @@ class DocumentTransactions extends ApiController
|
|||
*/
|
||||
public function index($document_id)
|
||||
{
|
||||
$transactions = Transaction::documentId($document_id)->get();
|
||||
$transactions = Transaction::with(['document', 'taxes'])->documentId($document_id)->get();
|
||||
|
||||
return Resource::collection($transactions);
|
||||
}
|
||||
|
|
@ -46,7 +47,7 @@ class DocumentTransactions extends ApiController
|
|||
*/
|
||||
public function show($document_id, $id)
|
||||
{
|
||||
$transaction = Transaction::documentId($document_id)->find($id);
|
||||
$transaction = Transaction::with(['document', 'taxes'])->documentId($document_id)->find($id);
|
||||
|
||||
if (! $transaction instanceof Transaction) {
|
||||
return $this->errorInternal('No query results for model [' . Transaction::class . '] ' . $id);
|
||||
|
|
@ -71,6 +72,25 @@ class DocumentTransactions extends ApiController
|
|||
return $this->created(route('api.documents.transactions.show', [$document_id, $transaction->id]), new Resource($transaction));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param $document_id
|
||||
* @param $id
|
||||
* @param $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function update($document_id, $id, Request $request)
|
||||
{
|
||||
$document = Document::find($document_id);
|
||||
|
||||
$transaction = Transaction::documentId($document_id)->find($id);
|
||||
|
||||
$transaction = $this->dispatch(new UpdateBankingDocumentTransaction($document, $transaction, $request));
|
||||
|
||||
return $this->created(route('api.documents.transactions.show', [$document_id, $transaction->id]), new Resource($transaction));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class Documents extends ApiController
|
|||
*/
|
||||
public function index()
|
||||
{
|
||||
$documents = Document::with('contact', 'histories', 'items', 'transactions')->collect(['issued_at'=> 'desc']);
|
||||
$documents = Document::with('contact', 'histories', 'items', 'item_taxes', 'totals', 'transactions')->collect(['issued_at'=> 'desc']);
|
||||
|
||||
return Resource::collection($documents);
|
||||
}
|
||||
|
|
@ -34,9 +34,33 @@ class Documents extends ApiController
|
|||
{
|
||||
// Check if we're querying by id or number
|
||||
if (is_numeric($id)) {
|
||||
$document = Document::find($id);
|
||||
$document = Document::with([
|
||||
'contact',
|
||||
'histories',
|
||||
'items',
|
||||
'items.taxes',
|
||||
'items.taxes.tax',
|
||||
'item_taxes',
|
||||
'totals',
|
||||
'transactions',
|
||||
'transactions.currency',
|
||||
'transactions.account',
|
||||
'transactions.category',
|
||||
])->find($id);
|
||||
} else {
|
||||
$document = Document::where('document_number', $id)->first();
|
||||
$document = Document::with([
|
||||
'contact',
|
||||
'histories',
|
||||
'items',
|
||||
'items.taxes',
|
||||
'items.taxes.tax',
|
||||
'item_taxes',
|
||||
'totals',
|
||||
'transactions',
|
||||
'transactions.currency',
|
||||
'transactions.account',
|
||||
'transactions.category',
|
||||
])->where('document_number', $id)->first();
|
||||
}
|
||||
|
||||
if (! $document instanceof Document) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use App\Models\Setting\Tax;
|
|||
use App\Traits\Currencies;
|
||||
use App\Traits\DateTime;
|
||||
use App\Traits\Transactions as TransactionsTrait;
|
||||
use App\Utilities\Date;
|
||||
|
||||
class RecurringTransactions extends Controller
|
||||
{
|
||||
|
|
@ -99,7 +100,9 @@ class RecurringTransactions extends Controller
|
|||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$response = $this->ajaxDispatch(new CreateTransaction($request->merge(['paid_at' => $request->get('recurring_started_at')])));
|
||||
$request->merge(['paid_at' => $request->get('recurring_started_at')]);
|
||||
|
||||
$response = $this->ajaxDispatch(new CreateTransaction($request));
|
||||
|
||||
if ($response['success']) {
|
||||
$response['redirect'] = route('recurring-transactions.show', $response['data']->id);
|
||||
|
|
@ -203,7 +206,9 @@ class RecurringTransactions extends Controller
|
|||
*/
|
||||
public function update(Transaction $recurring_transaction, Request $request)
|
||||
{
|
||||
$response = $this->ajaxDispatch(new UpdateTransaction($recurring_transaction, $request->merge(['paid_at' => $request->get('recurring_started_at')])));
|
||||
$request->merge(['paid_at' => $request->get('recurring_started_at')]);
|
||||
|
||||
$response = $this->ajaxDispatch(new UpdateTransaction($recurring_transaction, $request));
|
||||
|
||||
if ($response['success']) {
|
||||
$response['redirect'] = route('recurring-transactions.show', $recurring_transaction->id);
|
||||
|
|
|
|||
|
|
@ -239,6 +239,19 @@ class Transactions extends Controller
|
|||
*/
|
||||
public function update(Transaction $transaction, Request $request)
|
||||
{
|
||||
if ($transaction->document_id) {
|
||||
$message = trans('transactions.messages.update_document_transaction');
|
||||
|
||||
flash($message)->error()->important();
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => true,
|
||||
'message' => $message,
|
||||
'redirect' => route('transactions.edit', $transaction->id),
|
||||
]);
|
||||
}
|
||||
|
||||
$response = $this->ajaxDispatch(new UpdateTransaction($transaction, $request));
|
||||
|
||||
if ($response['success']) {
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ class Items extends Controller
|
|||
$currency_code = default_currency();
|
||||
}
|
||||
|
||||
$autocomplete = Item::autocomplete([
|
||||
$autocomplete = Item::with('taxes')->autocomplete([
|
||||
'name' => $query
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,29 @@ class Categories extends Controller
|
|||
*/
|
||||
public function create(IRequest $request)
|
||||
{
|
||||
$type = $request->get('type', 'item');
|
||||
$type = $request->get('type', Category::ITEM_TYPE);
|
||||
|
||||
$category_types = $this->getTypeCategoryTypes($type);
|
||||
$hide_code_types = $this->hideCodeCategoryTypes($category_types);
|
||||
|
||||
$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($category_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();
|
||||
$type_group = count($category_types) > 1 ? true : false;
|
||||
$types = $this->getCategoryTypes(group: true, types: $category_types);
|
||||
|
||||
$html = view('modals.categories.create', compact('type', 'types', 'categories', 'type_group', 'hide_code_types'))->render();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
|
|
@ -61,7 +75,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));
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ class DocumentTransactions extends Controller
|
|||
*/
|
||||
public function create(Document $document)
|
||||
{
|
||||
$document->load(['totals', 'transactions']);
|
||||
|
||||
$currency = Currency::where('code', $document->currency_code)->first();
|
||||
|
||||
$paid = $document->paid;
|
||||
|
|
@ -149,6 +151,8 @@ class DocumentTransactions extends Controller
|
|||
*/
|
||||
public function edit(Document $document, Transaction $transaction)
|
||||
{
|
||||
$document->load(['totals', 'transactions']);
|
||||
|
||||
$currency = Currency::where('code', $document->currency_code)->first();
|
||||
|
||||
// if you edit transaction before remove transaction amount
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use App\Jobs\Document\UpdateDocument;
|
|||
use App\Models\Common\Recurring;
|
||||
use App\Models\Document\Document;
|
||||
use App\Traits\Documents;
|
||||
use App\Utilities\Date;
|
||||
|
||||
class RecurringBills extends Controller
|
||||
{
|
||||
|
|
@ -80,7 +81,11 @@ class RecurringBills extends Controller
|
|||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$response = $this->ajaxDispatch(new CreateDocument($request->merge(['issued_at' => $request->get('recurring_started_at')])));
|
||||
$issue_at = Date::parse($request->get('recurring_started_at'))->format('Y-m-d');
|
||||
|
||||
$request->merge(['issued_at' => $issue_at]);
|
||||
|
||||
$response = $this->ajaxDispatch(new CreateDocument($request));
|
||||
|
||||
if ($response['success']) {
|
||||
$response['redirect'] = route('recurring-bills.show', $response['data']->id);
|
||||
|
|
@ -163,7 +168,11 @@ class RecurringBills extends Controller
|
|||
*/
|
||||
public function update(Document $recurring_bill, Request $request)
|
||||
{
|
||||
$response = $this->ajaxDispatch(new UpdateDocument($recurring_bill, $request->merge(['issued_at' => $request->get('recurring_started_at')])));
|
||||
$issue_at = Date::parse($request->get('recurring_started_at'))->format('Y-m-d');
|
||||
|
||||
$request->merge(['issued_at' => $issue_at]);
|
||||
|
||||
$response = $this->ajaxDispatch(new UpdateDocument($recurring_bill, $request));
|
||||
|
||||
if ($response['success']) {
|
||||
$response['redirect'] = route('recurring-bills.show', $response['data']->id);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,20 @@ class Vendors extends Controller
|
|||
*/
|
||||
public function index()
|
||||
{
|
||||
$vendors = Contact::with('media', 'bills.histories', 'bills.totals', 'bills.transactions', 'bills.media')->vendor()->collect();
|
||||
$vendors = Contact::with([
|
||||
'media',
|
||||
'bills.histories',
|
||||
'bills.totals',
|
||||
'bills.transactions',
|
||||
'bills.media'
|
||||
])
|
||||
->withCount([
|
||||
'contact_persons as contact_persons_with_email_count' => function ($query) {
|
||||
$query->whereNotNull('email');
|
||||
}
|
||||
])
|
||||
->vendor()
|
||||
->collect();
|
||||
|
||||
return $this->response('purchases.vendors.index', compact('vendors'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,20 @@ class Customers extends Controller
|
|||
*/
|
||||
public function index()
|
||||
{
|
||||
$customers = Contact::customer()->with('media', 'invoices.histories', 'invoices.totals', 'invoices.transactions', 'invoices.media')->collect();
|
||||
$customers = Contact::customer()
|
||||
->with([
|
||||
'media',
|
||||
'invoices.histories',
|
||||
'invoices.totals',
|
||||
'invoices.transactions',
|
||||
'invoices.media'
|
||||
])
|
||||
->withCount([
|
||||
'contact_persons as contact_persons_with_email_count' => function ($query) {
|
||||
$query->whereNotNull('email');
|
||||
}
|
||||
])
|
||||
->collect();
|
||||
|
||||
return $this->response('sales.customers.index', compact('customers'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class Invoices extends Controller
|
|||
{
|
||||
$this->setActiveTabForDocuments();
|
||||
|
||||
$invoices = Document::invoice()->with('contact', 'items', 'item_taxes', 'last_history', 'transactions', 'totals', 'histories', 'media')->collect(['document_number'=> 'desc']);
|
||||
$invoices = Document::invoice()->with('contact', 'items', 'items.taxes', 'item_taxes', 'last_history', 'transactions', 'totals', 'histories', 'media')->collect(['document_number'=> 'desc']);
|
||||
|
||||
$total_invoices = Document::invoice()->count();
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use App\Jobs\Document\UpdateDocument;
|
|||
use App\Models\Common\Recurring;
|
||||
use App\Models\Document\Document;
|
||||
use App\Traits\Documents;
|
||||
use App\Utilities\Date;
|
||||
|
||||
class RecurringInvoices extends Controller
|
||||
{
|
||||
|
|
@ -80,7 +81,11 @@ class RecurringInvoices extends Controller
|
|||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$response = $this->ajaxDispatch(new CreateDocument($request->merge(['issued_at' => $request->get('recurring_started_at')])));
|
||||
$issue_at = Date::parse($request->get('recurring_started_at'))->format('Y-m-d');
|
||||
|
||||
$request->merge(['issued_at' => $issue_at]);
|
||||
|
||||
$response = $this->ajaxDispatch(new CreateDocument($request));
|
||||
|
||||
if ($response['success']) {
|
||||
$response['redirect'] = route('recurring-invoices.show', $response['data']->id);
|
||||
|
|
@ -163,7 +168,11 @@ class RecurringInvoices extends Controller
|
|||
*/
|
||||
public function update(Document $recurring_invoice, Request $request)
|
||||
{
|
||||
$response = $this->ajaxDispatch(new UpdateDocument($recurring_invoice, $request->merge(['issued_at' => $request->get('recurring_started_at')])));
|
||||
$issue_at = Date::parse($request->get('recurring_started_at'))->format('Y-m-d');
|
||||
|
||||
$request->merge(['issued_at' => $issue_at]);
|
||||
|
||||
$response = $this->ajaxDispatch(new UpdateDocument($recurring_invoice, $request));
|
||||
|
||||
if ($response['success']) {
|
||||
$response['redirect'] = route('recurring-invoices.show', $response['data']->id);
|
||||
|
|
|
|||
|
|
@ -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,14 +81,17 @@ class Categories extends Controller
|
|||
*/
|
||||
public function create()
|
||||
{
|
||||
$types = $this->getCategoryTypes();
|
||||
|
||||
$categories = [];
|
||||
|
||||
foreach (config('type.category') as $type => $config) {
|
||||
$categories[$type] = [];
|
||||
}
|
||||
|
||||
$type_group = $this->isGroupCategoryType();
|
||||
$hide_code_types = $this->hideCodeCategoryTypes(array_keys($categories));
|
||||
|
||||
$types = $this->getCategoryTypes(group: $type_group);
|
||||
|
||||
Category::enabled()->orderBy('name')->get()->each(function ($category) use (&$categories) {
|
||||
$categories[$category->type][] = [
|
||||
'id' => $category->id,
|
||||
|
|
@ -70,7 +100,7 @@ class Categories extends Controller
|
|||
];
|
||||
});
|
||||
|
||||
return view('settings.categories.create', compact('types', 'categories'));
|
||||
return view('settings.categories.create', compact('types', 'categories', 'type_group', 'hide_code_types'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -134,8 +164,6 @@ class Categories extends Controller
|
|||
*/
|
||||
public function edit(Category $category)
|
||||
{
|
||||
$types = $this->getCategoryTypes();
|
||||
|
||||
$type_disabled = (Category::where('type', $category->type)->count() == 1) ?: false;
|
||||
|
||||
$edited_category_id = $category->id;
|
||||
|
|
@ -146,6 +174,11 @@ class Categories extends Controller
|
|||
$categories[$type] = [];
|
||||
}
|
||||
|
||||
$type_group = $this->isGroupCategoryType();
|
||||
$hide_code_types = $this->hideCodeCategoryTypes(array_keys($categories));
|
||||
|
||||
$types = $this->getCategoryTypes(group: $type_group);
|
||||
|
||||
$skip_categories = [];
|
||||
$skip_categories[] = $edited_category_id;
|
||||
|
||||
|
|
@ -175,7 +208,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_group', 'hide_code_types'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ class Kernel extends HttpKernel
|
|||
'menu.admin',
|
||||
'permission:read-admin-panel',
|
||||
'plan.limits',
|
||||
'module.subscription',
|
||||
],
|
||||
|
||||
'wizard' => [
|
||||
|
|
@ -175,6 +176,7 @@ class Kernel extends HttpKernel
|
|||
'dropzone' => \App\Http\Middleware\Dropzone::class,
|
||||
'header.x' => \App\Http\Middleware\AddXHeader::class,
|
||||
'plan.limits' => \App\Http\Middleware\RedirectIfHitPlanLimits::class,
|
||||
'module.subscription' => \App\Http\Middleware\RedirectIfHitModuleSubscription::class,
|
||||
'menu.admin' => \App\Http\Middleware\AdminMenu::class,
|
||||
'menu.portal' => \App\Http\Middleware\PortalMenu::class,
|
||||
'date.format' => \App\Http\Middleware\DateFormat::class,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Traits\Modules;
|
||||
use App\Utilities\Versions;
|
||||
use Closure;
|
||||
|
||||
class RedirectIfHitModuleSubscription
|
||||
{
|
||||
use Modules;
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if (! $request->isMethod(strtolower('GET'))) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if ($request->ajax()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if ($request->is(company_id() . '/apps/*')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (! $this->getModulesLimitOfSubscription()->action_status) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ class Transactions extends Import
|
|||
{
|
||||
$row = parent::map($row);
|
||||
|
||||
if (!isset($row['type'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$real_type = $this->getRealTypeTransaction($row['type']);
|
||||
$contact_type = config('type.transaction.' . $real_type . '.contact_type', $real_type == 'income' ? 'customer' : 'vendor');
|
||||
|
||||
|
|
@ -49,6 +53,7 @@ class Transactions extends Import
|
|||
public function prepareRules($rules): array
|
||||
{
|
||||
$rules['number'] = 'required|string';
|
||||
$rules['type'] = 'required|string';
|
||||
//$rules['currency_rate'] = 'required|gt:0';
|
||||
|
||||
return $rules;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ class Categories extends Import
|
|||
public $columns = [
|
||||
'name',
|
||||
'type',
|
||||
'code',
|
||||
'description',
|
||||
];
|
||||
|
||||
public function model(array $row)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ class UpdateUser extends Job implements ShouldUpdate
|
|||
$media = $this->getMedia($this->request->file('picture'), 'users');
|
||||
|
||||
$this->model->attachMedia($media, 'picture');
|
||||
} elseif (! $this->request->file('picture') && $this->model->picture) {
|
||||
} elseif ($this->request->isNotApi() && ! $this->request->file('picture') && $this->model->picture) {
|
||||
$this->deleteMediaModel($this->model, 'picture', $this->request);
|
||||
} elseif ($this->request->isApi() && $this->request->has('remove_picture') && $this->model->picture) {
|
||||
$this->deleteMediaModel($this->model, 'picture', $this->request);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,13 +38,6 @@ class CreateBankingDocumentTransaction extends Job implements ShouldCreate
|
|||
\DB::transaction(function () {
|
||||
$this->transaction = $this->dispatch(new CreateTransaction($this->request));
|
||||
|
||||
// Upload attachment
|
||||
if ($this->request->file('attachment')) {
|
||||
$media = $this->getMedia($this->request->file('attachment'), 'transactions');
|
||||
|
||||
$this->transaction->attachMedia($media, 'attachment');
|
||||
}
|
||||
|
||||
$this->model->save();
|
||||
|
||||
$this->createHistory();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class UpdateBankingDocumentTransaction extends Job implements ShouldUpdate
|
|||
{
|
||||
use Currencies;
|
||||
|
||||
protected Transaction $transaction;
|
||||
|
||||
public function __construct(Document $model, Transaction $transaction, $request)
|
||||
{
|
||||
$this->model = $model;
|
||||
|
|
@ -37,13 +39,6 @@ class UpdateBankingDocumentTransaction extends Job implements ShouldUpdate
|
|||
\DB::transaction(function () {
|
||||
$this->transaction = $this->dispatch(new UpdateTransaction($this->transaction, $this->request));
|
||||
|
||||
// Upload attachment
|
||||
if ($this->request->file('attachment')) {
|
||||
$media = $this->getMedia($this->request->file('attachment'), 'transactions');
|
||||
|
||||
$this->transaction->attachMedia($media, 'attachment');
|
||||
}
|
||||
|
||||
$this->model->save();
|
||||
|
||||
$this->createHistory();
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ class UpdateTransaction extends Job implements ShouldUpdate
|
|||
|
||||
$this->model->attachMedia($media, 'attachment');
|
||||
}
|
||||
} elseif (! $this->request->file('attachment') && $this->model->attachment) {
|
||||
} elseif ($this->request->isNotApi() && ! $this->request->file('attachment') && $this->model->attachment) {
|
||||
$this->deleteMediaModel($this->model, 'attachment', $this->request);
|
||||
} elseif ($this->request->isApi() && $this->request->has('remove_attachment') && $this->model->attachment) {
|
||||
$this->deleteMediaModel($this->model, 'attachment', $this->request);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ class UpdateTransfer extends Job implements ShouldUpdate
|
|||
|
||||
$this->model->attachMedia($media, 'attachment');
|
||||
}
|
||||
} elseif (! $this->request->file('attachment') && $this->model->attachment) {
|
||||
} elseif ($this->request->isNotApi() && ! $this->request->file('attachment') && $this->model->attachment) {
|
||||
$this->deleteMediaModel($this->model, 'attachment', $this->request);
|
||||
} elseif ($this->request->isApi() && $this->request->has('remove_attachment') && $this->model->attachment) {
|
||||
$this->deleteMediaModel($this->model, 'attachment', $this->request);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,10 +28,14 @@ class UpdateContact extends Job implements ShouldUpdate
|
|||
|
||||
// Upload logo
|
||||
if ($this->request->file('logo')) {
|
||||
$this->deleteMediaModel($this->model, 'logo', $this->request);
|
||||
|
||||
$media = $this->getMedia($this->request->file('logo'), Str::plural($this->model->type));
|
||||
|
||||
$this->model->attachMedia($media, 'logo');
|
||||
} elseif (! $this->request->file('logo') && $this->model->logo) {
|
||||
} elseif ($this->request->isNotApi() && ! $this->request->file('logo') && $this->model->logo) {
|
||||
$this->deleteMediaModel($this->model, 'logo', $this->request);
|
||||
} elseif ($this->request->isApi() && $this->request->has('remove_logo') && $this->model->logo) {
|
||||
$this->deleteMediaModel($this->model, 'logo', $this->request);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,15 @@ class UpdateItem extends Job implements ShouldUpdate
|
|||
|
||||
// Upload picture
|
||||
if ($this->request->file('picture')) {
|
||||
$this->deleteMediaModel($this->model, 'picture', $this->request);
|
||||
|
||||
$media = $this->getMedia($this->request->file('picture'), 'items');
|
||||
|
||||
$this->model->attachMedia($media, 'picture');
|
||||
} elseif ($this->request->isNotApi() && ! $this->request->file('picture') && $this->model->picture) {
|
||||
$this->deleteMediaModel($this->model, 'picture', $this->request);
|
||||
} elseif ($this->request->isApi() && $this->request->has('remove_picture') && $this->model->picture) {
|
||||
$this->deleteMediaModel($this->model, 'picture', $this->request);
|
||||
}
|
||||
|
||||
$this->deleteRelationships($this->model, ['taxes']);
|
||||
|
|
|
|||
|
|
@ -238,11 +238,11 @@ class CreateDocumentItemsAndTotals extends Job implements HasOwner, HasSource, S
|
|||
// Set taxes
|
||||
foreach ((array) $document_item->item_taxes as $item_tax) {
|
||||
if (array_key_exists($item_tax['tax_id'], $taxes)) {
|
||||
$taxes[$item_tax['tax_id']]['amount'] += $item_tax['amount'];
|
||||
$taxes[$item_tax['tax_id']]['amount'] += round((float) $item_tax['amount'], $this->document->currency->precision);
|
||||
} else {
|
||||
$taxes[$item_tax['tax_id']] = [
|
||||
'name' => $item_tax['name'],
|
||||
'amount' => $item_tax['amount'],
|
||||
'amount' => round((float) $item_tax['amount'], $this->document->currency->precision),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,18 +18,21 @@ class UpdateDocument extends Job implements ShouldUpdate
|
|||
|
||||
public function handle(): Document
|
||||
{
|
||||
$this->authorize();
|
||||
|
||||
if (empty($this->request['amount'])) {
|
||||
$this->request['amount'] = 0;
|
||||
}
|
||||
|
||||
// Disable this lines for global discount issue fixed ( https://github.com/akaunting/akaunting/issues/2797 )
|
||||
if (! empty($this->request['discount'])) {
|
||||
$this->request['discount_rate'] = $this->request['discount'];
|
||||
}
|
||||
$this->request['discount_rate'] = $this->request['discount'] ?? null;
|
||||
|
||||
event(new DocumentUpdating($this->model, $this->request));
|
||||
|
||||
\DB::transaction(function () {
|
||||
// Track original contact_id to sync transactions if it changes
|
||||
$originalContactId = $this->model->contact_id;
|
||||
|
||||
\DB::transaction(function () use ($originalContactId) {
|
||||
// Upload attachment
|
||||
if ($this->request->file('attachment')) {
|
||||
$this->deleteMediaModel($this->model, 'attachment', $this->request);
|
||||
|
|
@ -39,7 +42,9 @@ class UpdateDocument extends Job implements ShouldUpdate
|
|||
|
||||
$this->model->attachMedia($media, 'attachment');
|
||||
}
|
||||
} elseif (! $this->request->file('attachment') && $this->model->attachment) {
|
||||
} elseif ($this->request->isNotApi() && ! $this->request->file('attachment') && $this->model->attachment) {
|
||||
$this->deleteMediaModel($this->model, 'attachment', $this->request);
|
||||
} elseif ($this->request->isApi() && $this->request->has('remove_attachment') && $this->model->attachment) {
|
||||
$this->deleteMediaModel($this->model, 'attachment', $this->request);
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +71,13 @@ class UpdateDocument extends Job implements ShouldUpdate
|
|||
|
||||
$this->model->update($this->request->all());
|
||||
|
||||
// Sync transaction contact_id if document contact changed
|
||||
if (isset($this->request['contact_id']) && $originalContactId != $this->request['contact_id']) {
|
||||
$this->model->transactions()->update([
|
||||
'contact_id' => $this->request['contact_id'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->model->updateRecurring($this->request->all());
|
||||
});
|
||||
|
||||
|
|
@ -73,4 +85,23 @@ class UpdateDocument extends Job implements ShouldUpdate
|
|||
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this action is applicable.
|
||||
*/
|
||||
public function authorize(): void
|
||||
{
|
||||
$lockedStatuses = ['sent', 'received', 'viewed', 'partial', 'paid', 'overdue', 'unpaid', 'cancelled'];
|
||||
|
||||
if (
|
||||
isset($this->request['contact_id']) &&
|
||||
(int) $this->request['contact_id'] !== (int) $this->model->contact_id &&
|
||||
in_array($this->model->status, $lockedStatuses)
|
||||
) {
|
||||
$type = Str::plural($this->model->type);
|
||||
$message = trans('messages.warning.contact_change', ['type' => trans_choice("general.$type", 1)]);
|
||||
|
||||
throw new \Exception($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ 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['categories'] = $this->getExpenseCategories();
|
||||
$event->class->filters['routes']['categories'] = ['categories.index', 'search=type:' . $this->getExpenseCategoryTypes('string') . ' enabled:1'];
|
||||
$event->class->filters['multiple']['categories'] = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ 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['categories'] = $this->getIncomeCategories();
|
||||
$event->class->filters['routes']['categories'] = ['categories.index', 'search=type:' . $this->getIncomeCategoryTypes('string') . ' enabled:1'];
|
||||
$event->class->filters['multiple']['categories'] = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,10 @@ class AddIncomeExpenseCategories extends Listener
|
|||
return;
|
||||
}
|
||||
|
||||
$event->class->filters['categories'] = $this->getIncomeExpenseCategories(true);
|
||||
$event->class->filters['routes']['categories'] = ['categories.index', 'search=type:income,expense enabled:1'];
|
||||
$types = array_merge($this->getIncomeCategoryTypes(), $this->getExpenseCategoryTypes());
|
||||
|
||||
$event->class->filters['categories'] = $this->getIncomeExpenseCategories();
|
||||
$event->class->filters['routes']['categories'] = ['categories.index', 'search=type:' . implode(',', $types) . ' enabled:1'];
|
||||
$event->class->filters['multiple']['categories'] = true;
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +71,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 +85,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 +104,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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ class AddSearchString extends Listener
|
|||
return;
|
||||
}
|
||||
|
||||
$old = old();
|
||||
$request = request()->all();
|
||||
$old = old() ?? [];
|
||||
$request = request()->all() ?? [];
|
||||
|
||||
if ($old || $request) {
|
||||
$input = request('search');
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -303,7 +303,11 @@ class Contact extends Model
|
|||
{
|
||||
if (! empty($this->email)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->contact_persons_with_email_count)) {
|
||||
return $this->contact_persons_with_email_count > 0;
|
||||
}
|
||||
|
||||
if ($this->contact_persons()->whereNotNull('email')->count()) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class Document extends Model
|
|||
|
||||
public function items()
|
||||
{
|
||||
return $this->hasMany('App\Models\Document\DocumentItem', 'document_id');
|
||||
return $this->hasMany('App\Models\Document\DocumentItem', 'document_id')->with('taxes');
|
||||
}
|
||||
|
||||
public function item_taxes()
|
||||
|
|
@ -274,14 +274,22 @@ class Document extends Model
|
|||
|
||||
public function getSentAtAttribute(string $value = null)
|
||||
{
|
||||
$sent = $this->histories()->where('document_histories.status', 'sent')->first();
|
||||
if ($this->relationLoaded('histories')) {
|
||||
$sent = $this->histories->where('status', 'sent')->first();
|
||||
} else {
|
||||
$sent = $this->histories()->where('document_histories.status', 'sent')->first();
|
||||
}
|
||||
|
||||
return $sent->created_at ?? null;
|
||||
}
|
||||
|
||||
public function getReceivedAtAttribute(string $value = null)
|
||||
{
|
||||
$received = $this->histories()->where('document_histories.status', 'received')->first();
|
||||
if ($this->relationLoaded('histories')) {
|
||||
$received = $this->histories->where('status', 'received')->first();
|
||||
} else {
|
||||
$received = $this->histories()->where('document_histories.status', 'received')->first();
|
||||
}
|
||||
|
||||
return $received->created_at ?? null;
|
||||
}
|
||||
|
|
@ -347,7 +355,11 @@ class Document extends Model
|
|||
if ($discount) {
|
||||
$sub_total = $this->totals->where('code', 'sub_total')->makeHidden('title')->pluck('amount')->first();
|
||||
|
||||
$percent = number_format((($discount * 100) / $sub_total), 0);
|
||||
if ($sub_total && $sub_total > 0) {
|
||||
$percent = number_format((($discount * 100) / $sub_total), 0);
|
||||
} else {
|
||||
$percent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $percent;
|
||||
|
|
@ -374,6 +386,11 @@ class Document extends Model
|
|||
$rate = $this->currency_rate;
|
||||
$precision = currency($code)->getPrecision();
|
||||
|
||||
// Lazy eager load transactions if not already loaded to prevent N+1 queries
|
||||
if (!$this->relationLoaded('transactions')) {
|
||||
$this->load('transactions');
|
||||
}
|
||||
|
||||
if ($this->transactions->count()) {
|
||||
foreach ($this->transactions as $transaction) {
|
||||
$amount = $transaction->amount;
|
||||
|
|
@ -406,6 +423,11 @@ class Document extends Model
|
|||
$rate = $this->currency_rate;
|
||||
$precision = currency($code)->getPrecision();
|
||||
|
||||
// Lazy eager load transactions if not already loaded to prevent N+1 queries
|
||||
if (!$this->relationLoaded('transactions')) {
|
||||
$this->load('transactions');
|
||||
}
|
||||
|
||||
if ($this->transactions->count()) {
|
||||
foreach ($this->transactions as $transaction) {
|
||||
$amount = $transaction->amount;
|
||||
|
|
@ -415,7 +437,7 @@ class Document extends Model
|
|||
}
|
||||
|
||||
if ($transaction->reconciled) {
|
||||
$reconciled_amount = +$amount;
|
||||
$reconciled_amount += $amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,91 @@ class Category extends Model
|
|||
/**
|
||||
* Get the display name of the category.
|
||||
*/
|
||||
public function getDisplayNameAttribute()
|
||||
public function getDisplayNameAttribute(): string
|
||||
{
|
||||
return $this->name . ' (' . ucfirst($this->type) . ')';
|
||||
$hideCode = $this->hideCodeCategoryType($this->type);
|
||||
$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 +414,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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -126,8 +126,12 @@ class TaxSummary extends Report
|
|||
}
|
||||
|
||||
if ($date_field == 'paid_at') {
|
||||
$rate = ($item->amount * 100) / $type_item->amount;
|
||||
$item_amount = ($item_total->amount / 100) * $rate;
|
||||
if ($type_item->amount != 0) {
|
||||
$rate = ($item->amount * 100) / $type_item->amount;
|
||||
$item_amount = ($item_total->amount / 100) * $rate;
|
||||
} else {
|
||||
$item_amount = $item_total->amount;
|
||||
}
|
||||
} else {
|
||||
$item_amount = $item_total->amount;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,158 @@ use Illuminate\Support\Str;
|
|||
|
||||
trait Categories
|
||||
{
|
||||
public function getCategoryTypes(bool $translate = true): array
|
||||
public function isIncomeCategory(): bool
|
||||
{
|
||||
$types = [];
|
||||
$configs = config('type.category');
|
||||
$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 getTypeCategoryTypes(string $type, string $return = 'array'): string|array
|
||||
{
|
||||
switch ($type) {
|
||||
case Category::INCOME_TYPE:
|
||||
$types = $this->getIncomeCategoryTypes($return);
|
||||
break;
|
||||
case Category::EXPENSE_TYPE:
|
||||
$types = $this->getExpenseCategoryTypes($return);
|
||||
break;
|
||||
case Category::ITEM_TYPE:
|
||||
$types = $this->getItemCategoryTypes($return);
|
||||
break;
|
||||
case Category::OTHER_TYPE:
|
||||
$types = $this->getOtherCategoryTypes($return);
|
||||
break;
|
||||
default:
|
||||
$types = ($return == 'array') ? [$type] : $type;
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
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 isGroupCategoryType(): bool
|
||||
{
|
||||
$setting_category_types = setting('category.type');
|
||||
|
||||
foreach ($setting_category_types as $type => $category) {
|
||||
$categories = explode(',', $category);
|
||||
|
||||
if (count($categories) > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hideCodeCategoryType(string $type, bool $default = true): bool
|
||||
{
|
||||
return $this->hideCodeCategoryTypes($type)[$type] ?? $default;
|
||||
}
|
||||
|
||||
public function hideCodeCategoryTypes(string|array $types): array
|
||||
{
|
||||
$types = is_string($types) ? explode(',', $types) : $types;
|
||||
|
||||
$type_codes = [];
|
||||
|
||||
foreach ($types as $type) {
|
||||
$config_type = config('type.category.' . $type, []);
|
||||
|
||||
$type_codes[$type] = ! empty($config_type['hide']) && in_array('code', $config_type['hide']) ? true : false;
|
||||
}
|
||||
|
||||
return $type_codes;
|
||||
}
|
||||
|
||||
public function getCategoryTypes(bool $translate = true, bool $group = false, array $types = []): array
|
||||
{
|
||||
$category_types = [];
|
||||
|
||||
$configs = empty($types) ? config('type.category') : array_intersect_key(config('type.category'), array_flip($types));
|
||||
|
||||
foreach ($configs as $type => $attr) {
|
||||
$plural_type = Str::plural($type);
|
||||
|
|
@ -22,10 +170,47 @@ trait Categories
|
|||
$name = $attr['alias'] . '::' . $name;
|
||||
}
|
||||
|
||||
$types[$type] = $translate ? trans_choice($name, 1) : $name;
|
||||
if ($group) {
|
||||
$group_key = $attr['group'] ?? $type;
|
||||
|
||||
$category_types[$group_key][$type] = $translate ? trans_choice($name, 1) : $name;
|
||||
} else {
|
||||
$category_types[$type] = $translate ? trans_choice($name, 1) : $name;
|
||||
}
|
||||
}
|
||||
|
||||
return $types;
|
||||
return $category_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
|
||||
|
|
@ -36,7 +221,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 +247,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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ trait DateTime
|
|||
return [$start, $end];
|
||||
}
|
||||
|
||||
public function getFinancialStart($year = null): Date
|
||||
public function getFinancialStart($year = null, $date = null): Date
|
||||
{
|
||||
$start_of_year = Date::now()->startOfYear();
|
||||
|
||||
|
|
@ -64,9 +64,8 @@ trait DateTime
|
|||
$year = $year ?? $start_of_year->year;
|
||||
|
||||
$financial_start = Date::create($year, $month, $day);
|
||||
|
||||
if ((setting('localisation.financial_denote') == 'ends') && ($financial_start->dayOfYear != 1) ||
|
||||
$financial_start->greaterThan(Date::now())
|
||||
if ((setting('localisation.financial_denote') == 'ends') && ($financial_start->dayOfYear != 1) ||
|
||||
$financial_start->greaterThan($date ?? Date::now())
|
||||
) {
|
||||
$financial_start->subYear();
|
||||
}
|
||||
|
|
@ -74,10 +73,10 @@ trait DateTime
|
|||
return $financial_start;
|
||||
}
|
||||
|
||||
public function getFinancialWeek($year = null): CarbonPeriod
|
||||
public function getFinancialWeek($year = null, $date = null): CarbonPeriod
|
||||
{
|
||||
$today = Date::today();
|
||||
$financial_weeks = $this->getFinancialWeeks($year);
|
||||
$financial_weeks = $this->getFinancialWeeks($year, $date);
|
||||
|
||||
foreach ($financial_weeks as $week) {
|
||||
if ($today->lessThan($week->getStartDate()) || $today->greaterThan($week->getEndDate())) {
|
||||
|
|
@ -96,10 +95,10 @@ trait DateTime
|
|||
return $this_week;
|
||||
}
|
||||
|
||||
public function getFinancialMonth($year = null): CarbonPeriod
|
||||
public function getFinancialMonth($year = null, $date = null): CarbonPeriod
|
||||
{
|
||||
$today = Date::today();
|
||||
$financial_months = $this->getFinancialMonths($year);
|
||||
$financial_months = $this->getFinancialMonths($year, $date);
|
||||
|
||||
foreach ($financial_months as $month) {
|
||||
if ($today->lessThan($month->getStartDate()) || $today->greaterThan($month->getEndDate())) {
|
||||
|
|
@ -118,10 +117,10 @@ trait DateTime
|
|||
return $this_month;
|
||||
}
|
||||
|
||||
public function getFinancialQuarter($year = null): CarbonPeriod
|
||||
public function getFinancialQuarter($year = null, $date = null): CarbonPeriod
|
||||
{
|
||||
$today = Date::today();
|
||||
$financial_quarters = $this->getFinancialQuarters($year);
|
||||
$financial_quarters = $this->getFinancialQuarters($year, $date);
|
||||
|
||||
foreach ($financial_quarters as $quarter) {
|
||||
if ($today->lessThan($quarter->getStartDate()) || $today->greaterThan($quarter->getEndDate())) {
|
||||
|
|
@ -140,23 +139,23 @@ trait DateTime
|
|||
return $this_quarter;
|
||||
}
|
||||
|
||||
public function getFinancialYear($year = null): CarbonPeriod
|
||||
public function getFinancialYear($year = null, $date = null): CarbonPeriod
|
||||
{
|
||||
$financial_start = $this->getFinancialStart($year);
|
||||
$financial_start = $this->getFinancialStart($year, $date);
|
||||
|
||||
return CarbonPeriod::create($financial_start, $financial_start->copy()->addYear()->subDay()->endOfDay());
|
||||
}
|
||||
|
||||
public function getFinancialWeeks($year = null): array
|
||||
public function getFinancialWeeks($year = null, $date = null): array
|
||||
{
|
||||
$weeks = [];
|
||||
|
||||
$start = $this->getFinancialStart($year);
|
||||
$start = $this->getFinancialStart($year, $date);
|
||||
|
||||
$w = 52;
|
||||
|
||||
if (request()->filled('start_date') && request()->filled('end_date')) {
|
||||
$w = Date::parse(request('start_date'))->diffInWeeks(Date::parse(request('end_date'))) + 1;
|
||||
$w = Date::parse($start->copy())->diffInWeeks(Date::parse(request('end_date'))) + 1;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $w; $i++) {
|
||||
|
|
@ -166,16 +165,16 @@ trait DateTime
|
|||
return $weeks;
|
||||
}
|
||||
|
||||
public function getFinancialMonths($year = null): array
|
||||
public function getFinancialMonths($year = null, $date = null): array
|
||||
{
|
||||
$months = [];
|
||||
|
||||
$start = $this->getFinancialStart($year);
|
||||
$start = $this->getFinancialStart($year, $date);
|
||||
|
||||
$m = 12;
|
||||
|
||||
if (request()->filled('start_date') && request()->filled('end_date')) {
|
||||
$m = Date::parse(request('start_date'))->diffInMonths(Date::parse(request('end_date'))) + 1;
|
||||
$m = Date::parse($start->copy())->diffInMonths(Date::parse(request('end_date'))) + 1;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $m; $i++) {
|
||||
|
|
@ -185,16 +184,21 @@ trait DateTime
|
|||
return $months;
|
||||
}
|
||||
|
||||
public function getFinancialQuarters($year = null): array
|
||||
public function getFinancialQuarters($year = null, $date = null): array
|
||||
{
|
||||
$quarters = [];
|
||||
|
||||
$start = $this->getFinancialStart($year);
|
||||
$start = $this->getFinancialStart($year, $date);
|
||||
|
||||
$q = 4;
|
||||
|
||||
/*
|
||||
Previously, diffInQuarters was calculated from start_date, which caused errors
|
||||
when the financial start value differed from the default value.
|
||||
Therefore, this change has been made. Ticket: #8106
|
||||
*/
|
||||
if (request()->filled('start_date') && request()->filled('end_date')) {
|
||||
$q = Date::parse(request('start_date'))->diffInQuarters(Date::parse(request('end_date'))) + 1;
|
||||
$q = Date::parse($start->copy())->diffInQuarters(Date::parse(request('end_date'))) + 1;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $q; $i++) {
|
||||
|
|
@ -285,7 +289,7 @@ trait DateTime
|
|||
|
||||
switch ($period) {
|
||||
case 'yearly':
|
||||
$financial_year = $this->getFinancialYear($year);
|
||||
$financial_year = $this->getFinancialYear($year, $date);
|
||||
|
||||
if ($date->greaterThanOrEqualTo($financial_year->getStartDate()) && $date->lessThanOrEqualTo($financial_year->getEndDate())) {
|
||||
if (setting('localisation.financial_denote') == 'begins') {
|
||||
|
|
@ -297,7 +301,7 @@ trait DateTime
|
|||
|
||||
break;
|
||||
case 'quarterly':
|
||||
$quarters = $this->getFinancialQuarters($year);
|
||||
$quarters = $this->getFinancialQuarters($year, $date);
|
||||
|
||||
foreach ($quarters as $quarter) {
|
||||
if ($date->lessThan($quarter->getStartDate()) || $date->greaterThan($quarter->getEndDate())) {
|
||||
|
|
@ -314,7 +318,7 @@ trait DateTime
|
|||
|
||||
break;
|
||||
case 'weekly':
|
||||
$weeks = $this->getFinancialWeeks($year);
|
||||
$weeks = $this->getFinancialWeeks($year, $date);
|
||||
|
||||
foreach ($weeks as $week) {
|
||||
if ($date->lessThan($week->getStartDate()) || $date->greaterThan($week->getEndDate())) {
|
||||
|
|
@ -366,4 +370,4 @@ trait DateTime
|
|||
{
|
||||
return $this->scopeDateFilter($query, $field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ trait Documents
|
|||
|
||||
public function isNotRecurringDocument(): bool
|
||||
{
|
||||
return ! $this->isRecurring();
|
||||
return ! $this->isRecurringDocument();
|
||||
}
|
||||
|
||||
public function getRecurringDocumentTypes() : array
|
||||
|
|
@ -206,7 +206,8 @@ trait Documents
|
|||
|
||||
$today = Date::today()->toDateString();
|
||||
|
||||
$documents = $documents ?: Document::type($type)->with('transactions')->future();
|
||||
// Eager load transactions with currency to prevent N+1 queries when calling getAmountConvertedToDefault()
|
||||
$documents = $documents ?: Document::type($type)->with(['transactions', 'transactions.currency'])->future();
|
||||
|
||||
$documents->each(function ($document) use (&$totals, $today) {
|
||||
if (! in_array($document->status, $this->getDocumentStatusesForFuture())) {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ trait Import
|
|||
event(new ImportViewCreated($import));
|
||||
|
||||
return [
|
||||
$import->view,
|
||||
$import->view,
|
||||
$import->data
|
||||
];
|
||||
}
|
||||
|
|
@ -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) ? Contact::CUSTOMER_TYPE : Contact::VENDOR_TYPE) : Contact::CUSTOMER_TYPE);
|
||||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Jobs\Install\DisableModule;
|
||||
use App\Jobs\Install\UninstallModule;
|
||||
use App\Models\Module\Module;
|
||||
use App\Traits\SiteApi;
|
||||
use App\Utilities\Date;
|
||||
use App\Utilities\Info;
|
||||
use App\Utilities\Versions;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
trait Modules
|
||||
|
|
@ -423,6 +426,94 @@ trait Modules
|
|||
return ! $this->moduleIsEnabled($alias);
|
||||
}
|
||||
|
||||
public function getModulesLimitOfSubscription()
|
||||
{
|
||||
$limit = new \stdClass();
|
||||
|
||||
$limit->action_status = true;
|
||||
$limit->view_status = true;
|
||||
$limit->message = "Success";
|
||||
|
||||
if (! config('app.installed') || running_in_test()) {
|
||||
return $limit;
|
||||
}
|
||||
|
||||
if (is_cloud()) {
|
||||
return $limit;
|
||||
}
|
||||
|
||||
$modules = module()->all();
|
||||
|
||||
$versions = Versions::all($modules);
|
||||
|
||||
foreach ($versions as $alias => $version) {
|
||||
if ($alias == 'core') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$module_limit = $this->getModuleLimitOfSubscription($alias, $version);
|
||||
|
||||
if ($module_limit->action_status === false) {
|
||||
$limit->action_status = false;
|
||||
$limit->view_status = false;
|
||||
$limit->message = $module_limit->message;
|
||||
|
||||
// Clear cache to reflect changes
|
||||
Cache::forget('updates');
|
||||
Cache::forget('versions');
|
||||
}
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
|
||||
public function getModuleLimitOfSubscription($alias, $version = null)
|
||||
{
|
||||
$limit = new \stdClass();
|
||||
|
||||
$limit->action_status = true;
|
||||
$limit->view_status = true;
|
||||
$limit->message = "Success";
|
||||
|
||||
if (empty($version)) {
|
||||
$version = Versions::getVersionByAlias($alias);
|
||||
}
|
||||
|
||||
if (! $version->subscription) {
|
||||
return $limit;
|
||||
}
|
||||
|
||||
if (! in_array($version->subscription->action_status, ['disabled', 'uninstalled'])) {
|
||||
return $limit;
|
||||
}
|
||||
|
||||
$module_companies = Module::allCompanies()->enabled()->alias($alias)->get();
|
||||
|
||||
if (! $module_companies->count()) {
|
||||
return $limit;
|
||||
}
|
||||
|
||||
$limit->action_status = false;
|
||||
$limit->view_status = false;
|
||||
$limit->message = "Not able to app $alias.";
|
||||
|
||||
foreach ($module_companies as $module) {
|
||||
switch ($version->subscription->action_status) {
|
||||
case 'disabled':
|
||||
dispatch(new DisableModule($alias, $module->company_id));
|
||||
break;
|
||||
case 'uninstalled':
|
||||
dispatch(new UninstallModule($alias, $module->company_id));
|
||||
break;
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
|
||||
public function loadSubscriptions()
|
||||
{
|
||||
$key = 'apps.subscriptions';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ trait Transactions
|
|||
|
||||
public function isNotRecurringTransaction(): bool
|
||||
{
|
||||
return ! $this->isRecurring();
|
||||
return ! $this->isRecurringTransaction();
|
||||
}
|
||||
|
||||
public function isTransferTransaction(): bool
|
||||
|
|
@ -194,7 +194,7 @@ trait Transactions
|
|||
'add_an' => trans('general.form.add_an', ['field' => trans_choice('general.' . Str::plural($document_type), 1)]),
|
||||
'transaction' => trans_choice('general.' . Str::plural($type), 1),
|
||||
'difference' => trans('general.difference'),
|
||||
'connect_tax' => trans('messages.warning.connect_tax', ['type' => $type]),
|
||||
'connect_tax' => trans('messages.warning.connect_tax', ['type' => $type]),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,11 +73,58 @@ 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();
|
||||
|
||||
foreach ($currencies as $currency) {
|
||||
// If currency is not set in config, add it
|
||||
if (! config("money.currencies.{$currency->code}")) {
|
||||
config(['money.currencies.' . $currency->code => [
|
||||
'code' => $currency->code,
|
||||
'subunit' => 100,
|
||||
]]);
|
||||
}
|
||||
|
||||
config(['money.currencies.' . $currency->code . '.name' => $currency->name]);
|
||||
config(['money.currencies.' . $currency->code . '.rate' => $currency->rate]);
|
||||
config(['money.currencies.' . $currency->code . '.precision' => $currency->precision]);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
namespace App\Utilities;
|
||||
|
||||
use App\Models\Module\Module;
|
||||
use App\Traits\SiteApi;
|
||||
use App\Traits\Jobs;
|
||||
use App\Jobs\Install\DisableModule;
|
||||
use App\Jobs\Install\UninstallModule;
|
||||
use App\Utilities\Date;
|
||||
use GrahamCampbell\Markdown\Facades\Markdown;
|
||||
use Illuminate\Support\Arr;
|
||||
|
|
@ -10,7 +14,7 @@ use Illuminate\Support\Facades\Cache;
|
|||
|
||||
class Versions
|
||||
{
|
||||
use SiteApi;
|
||||
use SiteApi, Jobs;
|
||||
|
||||
public static function changelog()
|
||||
{
|
||||
|
|
@ -174,6 +178,7 @@ class Versions
|
|||
$version->latest = $latest;
|
||||
$version->errors = false;
|
||||
$version->message = '';
|
||||
$version->subscription = null;
|
||||
|
||||
if (! $body = static::getResponseBody('GET', $url, ['timeout' => 10])) {
|
||||
return $version;
|
||||
|
|
@ -187,10 +192,60 @@ class Versions
|
|||
$version->latest = $body->data->latest;
|
||||
$version->errors = $body->errors;
|
||||
$version->message = $body->message;
|
||||
$version->subscription = $body->data?->subscription ?? null;
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
public static function enforceSubscriptionStatus($alias, $version): void
|
||||
{
|
||||
if (! $version->subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($version->subscription->expired_at > Date::now()->startOfDay()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$status = 'active';
|
||||
|
||||
switch ($version->subscription->status) {
|
||||
case 'expired':
|
||||
case 'canceled':
|
||||
$status = 'disabled';
|
||||
break;
|
||||
case 'chargeback':
|
||||
case 'not_paid':
|
||||
case 'none':
|
||||
case 'refund':
|
||||
$status = 'uninstalled';
|
||||
break;
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
|
||||
if ($status == 'active') {
|
||||
return;
|
||||
}
|
||||
|
||||
$module_companies = Module::allCompanies()->alias($alias)->get();
|
||||
|
||||
foreach ($module_companies as $module) {
|
||||
switch ($status) {
|
||||
case 'disabled':
|
||||
dispatch(new DisableModule($alias, $module->company_id));
|
||||
break;
|
||||
case 'uninstalled':
|
||||
dispatch(new UninstallModule($alias, $module->company_id));
|
||||
break;
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getUpdates()
|
||||
{
|
||||
return Cache::remember('updates', Date::now()->addHours(6), function () {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ class Content extends Component
|
|||
// Handle documents
|
||||
$docs = $this->contact->isCustomer() ? 'invoices' : 'bills';
|
||||
|
||||
$this->documents = $this->contact->$docs()->with('transactions')->get();
|
||||
// Eager load documents with necessary relations to prevent lazy loading
|
||||
$this->documents = $this->contact->$docs()
|
||||
->with(['transactions', 'transactions.currency', 'contact', 'last_history', 'items', 'totals'])
|
||||
->get();
|
||||
|
||||
$this->counts['documents'] = $this->documents->count();
|
||||
|
||||
|
|
@ -62,8 +65,10 @@ class Content extends Component
|
|||
}
|
||||
}
|
||||
|
||||
// Handle payments
|
||||
$this->transactions = $this->contact->transactions()->with('account', 'category')->get();
|
||||
// Handle payments - eager load necessary relations to prevent lazy loading
|
||||
$this->transactions = $this->contact->transactions()
|
||||
->with(['account', 'category', 'currency', 'contact', 'document', 'recurring'])
|
||||
->get();
|
||||
|
||||
$this->counts['transactions'] = $this->transactions->count();
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,16 @@ class Information extends Component
|
|||
) {
|
||||
$this->document = $document;
|
||||
$this->hideShow = $hideShow;
|
||||
|
||||
// Load relations if not loaded to prevent lazy loading
|
||||
$relations = ['contact', 'last_history', 'items', 'totals'];
|
||||
|
||||
foreach ($relations as $relation) {
|
||||
if (! $document->relationLoaded($relation)) {
|
||||
$document->load($relation);
|
||||
}
|
||||
}
|
||||
|
||||
$this->showRoute = $this->getShowRoute($document->contact->type, $showRoute);
|
||||
$this->showDocumentRoute = $this->getShowRoute($document->type, $showDocumentRoute);
|
||||
$this->placement = (! empty($placement)) ? $placement : 'left';
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ class Attachment extends Component
|
|||
{
|
||||
$this->transaction_attachment = collect();
|
||||
|
||||
// Eager load transactions with their media/attachments to prevent N+1 queries
|
||||
if (!$this->document->relationLoaded('transactions')) {
|
||||
$this->document->load(['transactions.media', 'transactions']);
|
||||
}
|
||||
|
||||
if ($this->document->transactions->count()) {
|
||||
foreach ($this->document->transactions as $transaction) {
|
||||
if (! $transaction->attachment) {
|
||||
|
|
|
|||
|
|
@ -297,6 +297,7 @@ class EmptyPage extends Component
|
|||
'text' => trans('general.title.new', ['type' => $title]),
|
||||
'description' => trans('general.empty.actions.new', ['type' => strtolower($title)]),
|
||||
'active_badge' => true,
|
||||
'stack' => 'create_button',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -317,6 +318,7 @@ class EmptyPage extends Component
|
|||
'text' => trans('import.title', ['type' => $title]),
|
||||
'description' => trans('general.empty.actions.import', ['type' => strtolower($title)]),
|
||||
'active_badge' => false,
|
||||
'stack' => 'import_button',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class Account extends Form
|
|||
$this->selected = $account_id;
|
||||
|
||||
if (! $this->accounts->has($account_id)) {
|
||||
$account = Model::with('transactions')->find($account_id);
|
||||
$account = Model::with(['transactions', 'income_transactions', 'expense_transactions', 'currency'])->find($account_id);
|
||||
|
||||
$this->accounts->push($account);
|
||||
}
|
||||
|
|
@ -69,9 +69,9 @@ class Account extends Form
|
|||
protected function getAccounts()
|
||||
{
|
||||
if ($this->hideCurrency) {
|
||||
return Model::with('transactions')->enabled()->orderBy('name')->get();
|
||||
return Model::with(['transactions', 'income_transactions', 'expense_transactions', 'currency'])->enabled()->orderBy('name')->get();
|
||||
}
|
||||
|
||||
return Model::with('transactions')->enabled()->orderBy('name')->get();
|
||||
return Model::with(['transactions', 'income_transactions', 'expense_transactions', 'currency'])->enabled()->orderBy('name')->get();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,119 @@ 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']);
|
||||
$types = $this->getTypeCategoryTypes($this->type);
|
||||
$types_string = implode(',', $types);
|
||||
|
||||
$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:' . $types_string . ' enabled:1']);
|
||||
|
||||
$typeLabels = $this->getCategoryTypes(types: $types);
|
||||
|
||||
$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 +167,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Components\Index;
|
||||
|
||||
use App\Abstracts\View\Component;
|
||||
|
||||
class Balance extends Component
|
||||
{
|
||||
/**
|
||||
* The balance amount.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
public $amount;
|
||||
|
||||
/**
|
||||
* The text color class.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $textColor;
|
||||
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($amount = 0) {
|
||||
$this->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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,13 @@ class AccountBalance extends Widget
|
|||
public $report_class = 'App\Reports\IncomeExpense';
|
||||
|
||||
public function show()
|
||||
{
|
||||
$this->setData();
|
||||
|
||||
return $this->view('widgets.account_balance', $this->data);
|
||||
}
|
||||
|
||||
public function setData(): void
|
||||
{
|
||||
$accounts = Account::with('income_transactions', 'expense_transactions')->enabled()->take(5)->get()->map(function($account) {
|
||||
$account->balance_formatted = money($account->balance, $account->currency_code);
|
||||
|
|
@ -21,8 +28,8 @@ class AccountBalance extends Widget
|
|||
return $account;
|
||||
})->all();
|
||||
|
||||
return $this->view('widgets.account_balance', [
|
||||
$this->data = [
|
||||
'accounts' => $accounts,
|
||||
]);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ class CashFlow extends Widget
|
|||
public $period;
|
||||
|
||||
public function show()
|
||||
{
|
||||
$this->setData();
|
||||
|
||||
return $this->view('widgets.cash_flow', $this->data);
|
||||
}
|
||||
|
||||
public function setData(): void
|
||||
{
|
||||
$this->setFilter();
|
||||
|
||||
|
|
@ -66,10 +73,10 @@ class CashFlow extends Widget
|
|||
'profit_for_humans' => $profit_amount->formatForHumans(),
|
||||
];
|
||||
|
||||
return $this->view('widgets.cash_flow', [
|
||||
$this->data = [
|
||||
'chart' => $chart,
|
||||
'totals' => $totals,
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
public function setFilter(): void
|
||||
|
|
|
|||
|
|
@ -11,11 +11,17 @@ class Currencies extends Widget
|
|||
|
||||
public function show()
|
||||
{
|
||||
$currencies = Currency::enabled()->take(5)->get();
|
||||
$this->setData();
|
||||
|
||||
return $this->view('widgets.currencies', [
|
||||
'currencies' => $currencies,
|
||||
]);
|
||||
return $this->view('widgets.currencies', $this->data);
|
||||
}
|
||||
|
||||
}
|
||||
public function setData(): void
|
||||
{
|
||||
$currencies = Currency::enabled()->take(5)->get();
|
||||
|
||||
$this->data = [
|
||||
'currencies' => $currencies,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,13 @@ class ExpensesByCategory extends Widget
|
|||
public $report_class = 'App\Reports\ExpenseSummary';
|
||||
|
||||
public function show()
|
||||
{
|
||||
$this->setData();
|
||||
|
||||
return $this->view('widgets.donut_chart', $this->data);
|
||||
}
|
||||
|
||||
public function setData(): void
|
||||
{
|
||||
Category::with('expense_transactions')->expense()->withSubCategory()->getWithoutChildren()->each(function ($category) {
|
||||
$amount = 0;
|
||||
|
|
@ -30,8 +37,8 @@ class ExpensesByCategory extends Widget
|
|||
$chart->options['legend']['width'] = 160;
|
||||
$chart->options['legend']['position'] = 'right';
|
||||
|
||||
return $this->view('widgets.donut_chart', [
|
||||
$this->data = [
|
||||
'chart' => $chart,
|
||||
]);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ class Payables extends Widget
|
|||
public $report_class = 'Modules\AgedReceivablesPayables\Reports\AgedPayables';
|
||||
|
||||
public function show()
|
||||
{
|
||||
$this->setData();
|
||||
|
||||
return $this->view('widgets.receivables_payables', $this->data);
|
||||
}
|
||||
|
||||
public function setData(): void
|
||||
{
|
||||
$open = $overdue = 0;
|
||||
|
||||
|
|
@ -68,12 +75,12 @@ class Payables extends Widget
|
|||
|
||||
$grand_total_text = trans('widgets.total_unpaid_bills');
|
||||
|
||||
return $this->view('widgets.receivables_payables', [
|
||||
$this->data = [
|
||||
'totals' => $totals,
|
||||
'has_progress' => $has_progress,
|
||||
'progress' => $progress,
|
||||
'periods' => $periods,
|
||||
'grand_total_text' => $grand_total_text,
|
||||
]);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@ class ProfitLoss extends Widget
|
|||
public $period;
|
||||
|
||||
public function show()
|
||||
{
|
||||
$this->setData();
|
||||
|
||||
return $this->view('widgets.bar_chart', $this->data);
|
||||
}
|
||||
|
||||
public function setData(): void
|
||||
{
|
||||
$this->setFilter();
|
||||
|
||||
|
|
@ -44,9 +51,9 @@ class ProfitLoss extends Widget
|
|||
->setDataset(trans_choice('general.incomes', 1), 'column', array_values($this->getIncome()))
|
||||
->setDataset(trans_choice('general.expenses', 1), 'column', array_values($this->getExpense()));
|
||||
|
||||
return $this->view('widgets.bar_chart', [
|
||||
$this->data = [
|
||||
'chart' => $chart,
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
public function setFilter(): void
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ class Receivables extends Widget
|
|||
public $report_class = 'Modules\AgedReceivablesPayables\Reports\AgedReceivables';
|
||||
|
||||
public function show()
|
||||
{
|
||||
$this->setData();
|
||||
|
||||
return $this->view('widgets.receivables_payables', $this->data);
|
||||
}
|
||||
|
||||
public function setData(): void
|
||||
{
|
||||
$open = $overdue = 0;
|
||||
|
||||
|
|
@ -68,12 +75,12 @@ class Receivables extends Widget
|
|||
|
||||
$grand_total_text = trans('widgets.total_unpaid_invoices');
|
||||
|
||||
return $this->view('widgets.receivables_payables', [
|
||||
$this->data = [
|
||||
'totals' => $totals,
|
||||
'has_progress' => $has_progress,
|
||||
'progress' => $progress,
|
||||
'periods' => $periods,
|
||||
'grand_total_text' => $grand_total_text,
|
||||
]);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@
|
|||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-openssl": "*",
|
||||
|
|
@ -79,11 +79,11 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"beyondcode/laravel-dump-server": "^1.9",
|
||||
"brianium/paratest": "^7.1",
|
||||
"brianium/paratest": "^7.3.2",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"nunomaduro/collision": "^7.0",
|
||||
"phpunit/phpunit": "10.5.30",
|
||||
"phpunit/phpunit": "^10.5.63",
|
||||
"spatie/laravel-ignition": "^2.0",
|
||||
"wnx/laravel-stats": "^2.11"
|
||||
},
|
||||
|
|
@ -166,6 +166,9 @@
|
|||
"allow-plugins": {
|
||||
"mnsami/composer-custom-directory-installer": true,
|
||||
"php-http/discovery": true
|
||||
},
|
||||
"audit": {
|
||||
"ignore": ["PKSA-y2cr-5h3j-g3ys"]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -150,6 +150,11 @@ return [
|
|||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'sentry_logs' => [
|
||||
'driver' => 'sentry_logs',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Setting\Category;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|
|
@ -184,7 +186,7 @@ return [
|
|||
'payment_method',
|
||||
'reference',
|
||||
'category_id' => [
|
||||
'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' => [
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,11 +296,12 @@ 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' => [],
|
||||
'notification' => [
|
||||
|
||||
],
|
||||
|
|
@ -333,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' => [
|
||||
|
|
@ -342,7 +352,7 @@ return [
|
|||
'file' => 'transactions',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
Transaction::INCOME_TYPE => [
|
||||
'group' => 'banking',
|
||||
'route' => [
|
||||
|
|
@ -364,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' => [
|
||||
|
|
@ -395,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' => [
|
||||
|
|
@ -426,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',
|
||||
|
|
@ -488,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' => [
|
||||
|
|
@ -518,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' => [
|
||||
|
|
@ -548,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',
|
||||
|
|
|
|||
|
|
@ -10,15 +10,15 @@ return [
|
|||
|
||||
'minor' => '1',
|
||||
|
||||
'patch' => '19',
|
||||
'patch' => '21',
|
||||
|
||||
'build' => '',
|
||||
|
||||
'status' => 'Stable',
|
||||
|
||||
'date' => '22-July-2025',
|
||||
'date' => '13-December-2025',
|
||||
|
||||
'time' => '12:00',
|
||||
'time' => '22:00',
|
||||
|
||||
'zone' => 'GMT +3',
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('categories', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,21 @@
|
|||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @package Akaunting
|
||||
* @copyright 2017-2023 Akaunting. All rights reserved.
|
||||
* @license BSL; see LICENSE.txt
|
||||
* @link https://akaunting.com
|
||||
*/
|
||||
|
||||
use Illuminate\Contracts\Http\Kernel;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Livewire\Livewire;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
$_ENV['ASSET_URL'] = $_ENV['ASSET_URL'] ?? './';
|
||||
$_ENV['LIVEWIRE_ASSET_URL'] = $_ENV['LIVEWIRE_ASSET_URL'] ?? './';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Check If The Application Is Under Maintenance
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If the application is in maintenance / demo mode via the "down" command
|
||||
| we will load this file so that any pre-rendered content can be shown
|
||||
| instead of starting the framework, which could cause an exception.
|
||||
|
|
||||
*/
|
||||
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register The Auto Loader
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Composer provides a convenient, automatically generated class loader for
|
||||
| this application. We just need to utilize it! We'll simply require it
|
||||
| into the script here so we don't need to manually load our classes.
|
||||
|
|
||||
*/
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Run The Application
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Once we have the application, we can handle the incoming request using
|
||||
| the application's HTTP kernel. Then, we will send the response back
|
||||
| to this client's browser, allowing them to enjoy our application.
|
||||
|
|
||||
*/
|
||||
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->booted(function () {
|
||||
Livewire::setScriptRoute(function ($handle) {
|
||||
$base = request()->getBasePath();
|
||||
$base = str_replace('/public', '', $base);
|
||||
|
||||
return Route::get($base . '/vendor/livewire/livewire/dist/livewire.min.js', $handle);
|
||||
});
|
||||
});
|
||||
|
||||
$kernel = $app->make(Kernel::class);
|
||||
|
||||
$response = $kernel->handle(
|
||||
$request = Request::capture()
|
||||
)->send();
|
||||
|
||||
$kernel->terminate($request, $response);
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
default: 'sale'
|
||||
},
|
||||
|
||||
currecyCode: {
|
||||
currencyCode: {
|
||||
type: String,
|
||||
default: 'USD'
|
||||
},
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
},
|
||||
|
||||
currencySymbol: {
|
||||
default: {}
|
||||
default: {}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
if (conversion[0]) {
|
||||
this.texts.push(conversion[0]);
|
||||
}
|
||||
|
||||
|
||||
if (conversion[1]) {
|
||||
this.texts.push(conversion[1].replace(':currency_code', company_currency_code).replace(':currency_rate', ''));
|
||||
}
|
||||
|
|
@ -92,25 +92,25 @@
|
|||
|
||||
watch: {
|
||||
currencyConversionText: function (text) {
|
||||
this.conversion = text.replace(':price', this.price).replace(':currency_code', this.currecyCode);
|
||||
this.conversion = text.replace(':price', this.price).replace(':currency_code', this.currencyCode);
|
||||
},
|
||||
|
||||
price: function (price) {
|
||||
this.conversion = this.currencyConversionText.replace(':price', price).replace(':currency_code', this.currecyCode).replace();
|
||||
this.conversion = this.currencyConversionText.replace(':price', price).replace(':currency_code', this.currencyCode).replace();
|
||||
},
|
||||
|
||||
currecyCode: function (currecyCode) {
|
||||
this.conversion = this.currencyConversionText.replace(':price', this.price).replace(':currency_code', this.currecyCode).replace();
|
||||
currencyCode: function (currencyCode) {
|
||||
this.conversion = this.currencyConversionText.replace(':price', this.price).replace(':currency_code', this.currencyCode).replace();
|
||||
},
|
||||
|
||||
currencyRate: function (currencyRate) {
|
||||
this.rate = currencyRate;
|
||||
|
||||
this.conversion = this.currencyConversionText.replace(':price', this.price).replace(':currency_code', this.currecyCode).replace();
|
||||
this.conversion = this.currencyConversionText.replace(':price', this.price).replace(':currency_code', this.currencyCode).replace();
|
||||
},
|
||||
|
||||
currencySymbol: function (currencySymbol) {
|
||||
this.conversion = this.currencyConversionText.replace(':price', this.price).replace(':currency_code', this.currecyCode).replace();
|
||||
this.conversion = this.currencyConversionText.replace(':price', this.price).replace(':currency_code', this.currencyCode).replace();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
:collapse-tags="collapse"
|
||||
:remote-method="remoteMethod"
|
||||
:loading="loading"
|
||||
:class="[{ 'with-color-prefix': selectedOptionColor, 'with-icon-prefix': icon }]"
|
||||
>
|
||||
<div
|
||||
v-if="loading"
|
||||
|
|
@ -56,21 +57,24 @@
|
|||
<div v-if="!loading && addNew.status && options.length == 0">
|
||||
<el-option class="text-center" disabled :label="noDataText" value="value"></el-option>
|
||||
|
||||
<ul class="el-scrollbar__view el-select-dropdown__list">
|
||||
<li class="el-select-dropdown__item el-select__footer bg-purple sticky bottom-0">
|
||||
<div class="w-full flex items-center" @click="onAddItem">
|
||||
<span class="material-icons text-xl text-purple">add</span>
|
||||
<span class="flex-1 font-bold text-purple">
|
||||
{{ addNew.text }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<li class="el-select-dropdown__item el-select__footer bg-purple sticky bottom-0">
|
||||
<div class="w-full flex items-center" @click="onAddItem">
|
||||
<span class="material-icons text-xl text-purple">add</span>
|
||||
<span class="flex-1 font-bold text-purple">
|
||||
{{ addNew.text }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<template slot="prefix">
|
||||
<span class="el-input__suffix-inner el-select-icon">
|
||||
<i :class="'select-icon-position el-input__icon fa fa-' + icon"></i>
|
||||
<span class="aka-select-prefix">
|
||||
<span
|
||||
v-if="!isDropdownVisible && selectedOptionColor"
|
||||
class="w-4 h-4 rounded-full mt-1 ml-2"
|
||||
:style="{ backgroundColor: selectedOptionColor }"
|
||||
></span>
|
||||
<i v-if="icon" :class="'select-icon-position el-input__icon fa fa-' + icon"></i>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
@ -141,12 +145,19 @@
|
|||
|
||||
<component v-bind:is="add_new_html" @submit="onSubmit" @cancel="onCancel"></component>
|
||||
|
||||
<span slot="infoBlock" class="absolute right-8 top-3 bg-green text-white px-2 py-1 rounded-md text-xs" v-if="new_options[selected] || (sorted_options.length && sorted_options[sorted_options.length - 1].mark_new && sorted_options[sorted_options.length - 1].key == selected)">{{ addNew.new_text }}</span>
|
||||
<span slot="infoBlock" class="absolute right-8 top-3 bg-green text-white px-2 py-1 rounded-md text-xs" v-if="!isDropdownVisible && (new_options[selected] || (sorted_options.length && sorted_options[sorted_options.length - 1].mark_new && sorted_options[sorted_options.length - 1].key == selected))">{{ addNew.new_text }}</span>
|
||||
|
||||
<span
|
||||
slot="infoBlock"
|
||||
class="absolute right-8 top-4 rounded-md bg-gray-50 px-2 py-0.5 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10"
|
||||
v-if="!isDropdownVisible && group && selectedGroupLabel"
|
||||
>
|
||||
{{ selectedGroupLabel }}
|
||||
</span>
|
||||
|
||||
<select :name="name" :id="name + '-' + _uid" class="hidden">
|
||||
<option v-for="option in sortedOptions" :key="option.key" :value="option.key">{{ option.value }}</option>
|
||||
</select>
|
||||
|
||||
</base-input>
|
||||
|
||||
<span v-else>
|
||||
|
|
@ -159,6 +170,7 @@
|
|||
:collapse-tags="collapse"
|
||||
:remote-method="remoteMethod"
|
||||
:loading="loading"
|
||||
:class="[{ 'with-color-prefix': selectedOptionColor, 'with-icon-prefix': icon }]"
|
||||
>
|
||||
<div v-if="loading" class="el-select-dropdown__wrap" slot="empty">
|
||||
<p class="el-select-dropdown__empty pt-2 pb-0 loading">
|
||||
|
|
@ -185,21 +197,25 @@
|
|||
|
||||
<div v-if="!loading && addNew.status && options.length == 0">
|
||||
<el-option class="text-center" disabled :label="noDataText" value="value"></el-option>
|
||||
<ul class="el-scrollbar__view el-select-dropdown__list">
|
||||
<li class="el-select-dropdown__item el-select__footer bg-purple sticky bottom-0">
|
||||
<div class="w-full flex items-center" @click="onAddItem">
|
||||
<span class="material-icons text-xl text-purple">add</span>
|
||||
<span class="flex-1 font-bold text-purple">
|
||||
{{ addNew.text }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<li class="el-select-dropdown__item el-select__footer bg-purple sticky bottom-0">
|
||||
<div class="w-full flex items-center" @click="onAddItem">
|
||||
<span class="material-icons text-xl text-purple">add</span>
|
||||
<span class="flex-1 font-bold text-purple">
|
||||
{{ addNew.text }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<template slot="prefix">
|
||||
<span class="el-input__suffix-inner el-select-icon">
|
||||
<i :class="'select-icon-position el-input__icon fa fa-' + icon"></i>
|
||||
<span class="aka-select-prefix">
|
||||
<span
|
||||
v-if="selectedOptionColor"
|
||||
class="aka-select-prefix-dot"
|
||||
:style="{ backgroundColor: selectedOptionColor }"
|
||||
></span>
|
||||
<i v-if="icon" :class="'select-icon-position el-input__icon fa fa-' + icon"></i>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
@ -265,12 +281,19 @@
|
|||
</span>
|
||||
</div>
|
||||
</el-option>
|
||||
|
||||
</el-select>
|
||||
|
||||
<component v-bind:is="add_new_html" @submit="onSubmit" @cancel="onCancel"></component>
|
||||
|
||||
<span slot="infoBlock" class="absolute right-8 top-3 bg-green text-white px-2 py-1 rounded-md text-xs" v-if="new_options[selected] || (sorted_options.length && sorted_options[sorted_options.length - 1].mark_new && sorted_options[sorted_options.length - 1].key == selected)">{{ addNew.new_text }}</span>
|
||||
<span slot="infoBlock" class="absolute right-8 top-3 bg-green text-white px-2 py-1 rounded-md text-xs" v-if="!isDropdownVisible && (new_options[selected] || (sorted_options.length && sorted_options[sorted_options.length - 1].mark_new && sorted_options[sorted_options.length - 1].key == selected))">{{ addNew.new_text }}</span>
|
||||
|
||||
<span
|
||||
slot="infoBlock"
|
||||
class="absolute right-8 top-4 rounded-md bg-gray-50 px-2 py-0.5 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10"
|
||||
v-if="!isDropdownVisible && group && selectedGroupLabel"
|
||||
>
|
||||
{{ selectedGroupLabel }}
|
||||
</span>
|
||||
|
||||
<select :name="name" :id="name + '-' + _uid" v-model="selected" class="d-none">
|
||||
<option v-for="option in sortedOptions" :key="option.key" :value="option.key">{{ option.value }}</option>
|
||||
|
|
@ -491,7 +514,7 @@ export default {
|
|||
currencyCode: {
|
||||
type: String,
|
||||
default: 'USD',
|
||||
description: "Get remote item price currecy code"
|
||||
description: "Get remote item price currency code"
|
||||
},
|
||||
|
||||
optionStyle: {
|
||||
|
|
@ -520,6 +543,7 @@ export default {
|
|||
full_options:[],
|
||||
new_options: {},
|
||||
loading: false,
|
||||
isDropdownVisible: false,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -549,6 +573,48 @@ export default {
|
|||
|
||||
return this.sorted_options;
|
||||
},
|
||||
|
||||
selectedGroupLabel() {
|
||||
if (!this.group || !Array.isArray(this.sorted_options) || !this.sorted_options.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this.multiple) {
|
||||
if (!Array.isArray(this.selected) || !this.selected.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const labels = this.selected
|
||||
.map(value => this.findGroupLabelByOptionKey(value))
|
||||
.filter(Boolean);
|
||||
|
||||
return [...new Set(labels)].join(', ');
|
||||
}
|
||||
|
||||
return this.findGroupLabelByOptionKey(this.selected);
|
||||
},
|
||||
|
||||
selectedOptionColor() {
|
||||
const selectedOption = this.getSelectedOptionData();
|
||||
|
||||
if (!selectedOption || typeof selectedOption !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (selectedOption.color_hex_code) {
|
||||
return selectedOption.color_hex_code;
|
||||
}
|
||||
|
||||
if (selectedOption.color_hex) {
|
||||
return selectedOption.color_hex;
|
||||
}
|
||||
|
||||
if (selectedOption.color && selectedOption.color.toString().startsWith('#')) {
|
||||
return selectedOption.color;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
|
@ -579,6 +645,78 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
getSelectedOptionData() {
|
||||
const selectedKey = this.multiple
|
||||
? (Array.isArray(this.selected) && this.selected.length ? this.selected[0] : null)
|
||||
: this.selected;
|
||||
|
||||
if (selectedKey === null || selectedKey === undefined || selectedKey === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const foundOption = this.findOptionByKey(selectedKey);
|
||||
|
||||
if (!foundOption) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return foundOption.option ? foundOption.option : foundOption;
|
||||
},
|
||||
|
||||
findOptionByKey(optionKey) {
|
||||
const normalizedKey = optionKey.toString();
|
||||
|
||||
if (this.group) {
|
||||
for (const groupOption of this.sorted_options) {
|
||||
if (!Array.isArray(groupOption.value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const found = groupOption.value.find(option => option.key == normalizedKey);
|
||||
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const found = this.sorted_options.find(option => option.key == normalizedKey);
|
||||
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
const foundInFullOptions = this.full_options.find(option => option.key == normalizedKey);
|
||||
|
||||
if (foundInFullOptions) {
|
||||
return foundInFullOptions;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
findGroupLabelByOptionKey(optionKey) {
|
||||
if (optionKey === null || optionKey === undefined || optionKey === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const normalizedKey = optionKey.toString();
|
||||
|
||||
const foundGroup = this.sorted_options.find(groupOption => {
|
||||
if (!Array.isArray(groupOption.value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return groupOption.value.some(option => option.key == normalizedKey);
|
||||
});
|
||||
|
||||
if (!foundGroup) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return foundGroup.key ? foundGroup.key.toString() : '';
|
||||
},
|
||||
|
||||
sortBy(option) {
|
||||
return (firstEl, secondEl) => {
|
||||
let first_element = firstEl[option].toUpperCase(); // ignore upper and lowercase
|
||||
|
|
@ -910,6 +1048,8 @@ export default {
|
|||
visibleChange(event) {
|
||||
this.$emit('visible-change', event);
|
||||
|
||||
this.isDropdownVisible = event;
|
||||
|
||||
this.dynamicPlaceholder = this.placeholder;
|
||||
|
||||
if (event && this.searchText) {
|
||||
|
|
@ -1492,11 +1632,25 @@ export default {
|
|||
</script>
|
||||
|
||||
<style>
|
||||
.aka-select-prefix {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.with-color-prefix .el-input__inner {
|
||||
padding-left: 2.25rem !important;
|
||||
}
|
||||
|
||||
.with-color-prefix.with-icon-prefix .el-input__inner {
|
||||
padding-left: 2.8rem !important;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item.el-select__footer.bg-purple.sticky.bottom-0 {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item.el-select__footer.bg-purple.sticky.bottom-0:hover {
|
||||
background-color: 55588b !important;
|
||||
background-color: #55588b !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ export default {
|
|||
<div class="swiper-button-prev bg-body text-white flex items-center justify-center left-0">
|
||||
<span class="material-icons text-purple text-4xl">chevron_left</span>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
|
||||
item.querySelector('[data-tabs-swiper]').innerHTML = html;
|
||||
slides_view = Number(item.getAttribute('data-swiper')) != 0 ? Number(item.getAttribute('data-swiper')) : slides_view;
|
||||
|
|
@ -301,6 +301,28 @@ export default {
|
|||
this.form[key] = event.target.files[0];
|
||||
},
|
||||
|
||||
isCategoryCodeFieldVisible() {
|
||||
if (!this.form || !this.form.type || !this.form.type_codes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let type_codes = this.form.type_codes;
|
||||
|
||||
if (typeof type_codes === 'string') {
|
||||
try {
|
||||
type_codes = JSON.parse(type_codes);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof type_codes[this.form.type] === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !Boolean(type_codes[this.form.type]);
|
||||
},
|
||||
|
||||
// Bulk Action Select all
|
||||
onSelectAllBulkAction() {
|
||||
this.bulk_action.selectAll();
|
||||
|
|
@ -945,15 +967,19 @@ export default {
|
|||
let data = this.form.data();
|
||||
|
||||
FormData.prototype.appendRecursive = function(data, wrapper = null) {
|
||||
for(var name in data) {
|
||||
for (var name in data) {
|
||||
if (name == "previewElement" || name == "previewTemplate") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wrapper) {
|
||||
if ((typeof data[name] == 'object' || data[name].constructor === Array) && ((data[name] instanceof File != true ) && (data[name] instanceof Blob != true))) {
|
||||
if ((typeof data[name] == 'object' || Array.isArray(data[name])) && ((data[name] instanceof File != true ) && (data[name] instanceof Blob != true))) {
|
||||
this.appendRecursive(data[name], wrapper + '[' + name + ']');
|
||||
} else {
|
||||
this.append(wrapper + '[' + name + ']', data[name]);
|
||||
}
|
||||
} else {
|
||||
if ((typeof data[name] == 'object' || data[name].constructor === Array) && ((data[name] instanceof File != true ) && (data[name] instanceof Blob != true))) {
|
||||
if ((typeof data[name] == 'object' || Array.isArray(data[name])) && ((data[name] instanceof File != true ) && (data[name] instanceof Blob != true))) {
|
||||
this.appendRecursive(data[name], name);
|
||||
} else {
|
||||
this.append(name, data[name]);
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ const app = new Vue({
|
|||
if (inclusives.length) {
|
||||
inclusives.forEach(function(inclusive) {
|
||||
item.tax_ids[inclusive.tax_index].name = inclusive.tax_name;
|
||||
item.tax_ids[inclusive.tax_index].price = item.grand_total - (item.grand_total / (1 + inclusive.tax_rate / 100));
|
||||
item.tax_ids[inclusive.tax_index].price = this.numberFormat(item.grand_total - (item.grand_total / (1 + inclusive.tax_rate / 100)), this.currency.precision);
|
||||
|
||||
inclusive_tax_total += item.tax_ids[inclusive.tax_index].price;
|
||||
|
||||
|
|
@ -474,7 +474,7 @@ const app = new Vue({
|
|||
if (fixed.length) {
|
||||
fixed.forEach(function(fixed) {
|
||||
item.tax_ids[fixed.tax_index].name = fixed.tax_name;
|
||||
item.tax_ids[fixed.tax_index].price = fixed.tax_rate * calculationToQuantity(item.quantity);
|
||||
item.tax_ids[fixed.tax_index].price = this.numberFormat(fixed.tax_rate * calculationToQuantity(item.quantity), this.currency.precision);
|
||||
|
||||
total_tax_amount += item.tax_ids[fixed.tax_index].price;
|
||||
|
||||
|
|
@ -491,7 +491,7 @@ const app = new Vue({
|
|||
if (normal.length) {
|
||||
normal.forEach(function(normal) {
|
||||
item.tax_ids[normal.tax_index].name = normal.tax_name;
|
||||
item.tax_ids[normal.tax_index].price = price_for_tax * (normal.tax_rate / 100);
|
||||
item.tax_ids[normal.tax_index].price = this.numberFormat((price_for_tax * (normal.tax_rate / 100)), this.currency.precision);
|
||||
|
||||
total_tax_amount += item.tax_ids[normal.tax_index].price;
|
||||
|
||||
|
|
@ -502,7 +502,7 @@ const app = new Vue({
|
|||
if (withholding.length) {
|
||||
withholding.forEach(function(withholding) {
|
||||
item.tax_ids[withholding.tax_index].name = withholding.tax_name;
|
||||
item.tax_ids[withholding.tax_index].price = -(price_for_tax * (withholding.tax_rate / 100));
|
||||
item.tax_ids[withholding.tax_index].price = -this.numberFormat((price_for_tax * (withholding.tax_rate / 100)), this.currency.precision);
|
||||
|
||||
total_tax_amount += item.tax_ids[withholding.tax_index].price;
|
||||
|
||||
|
|
@ -515,7 +515,7 @@ const app = new Vue({
|
|||
if (compounds.length) {
|
||||
compounds.forEach(function(compound) {
|
||||
item.tax_ids[compound.tax_index].name = compound.tax_name;
|
||||
item.tax_ids[compound.tax_index].price = (item.grand_total / 100) * compound.tax_rate;
|
||||
item.tax_ids[compound.tax_index].price = this.numberFormat((item.grand_total / 100) * compound.tax_rate, this.currency.precision);
|
||||
|
||||
totals_taxes = this.calculateTotalsTax(totals_taxes, compound.tax_id, compound.tax_name, item.tax_ids[compound.tax_index].price);
|
||||
|
||||
|
|
@ -887,6 +887,16 @@ const app = new Vue({
|
|||
|
||||
this.onSubmit();
|
||||
},
|
||||
|
||||
numberFormat(number, decimals = 0) {
|
||||
number = parseFloat(number);
|
||||
|
||||
if (isNaN(number)) return parseFloat('0');
|
||||
|
||||
number = number.toFixed(decimals);
|
||||
|
||||
return parseFloat(number);
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const app = new Vue({
|
|||
form: new Form('category'),
|
||||
bulk_action: new BulkAction('categories'),
|
||||
categoriesBasedTypes: null,
|
||||
selected_type: true
|
||||
selected_type: true,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -291,6 +291,10 @@
|
|||
color:#595959 !important;
|
||||
}
|
||||
|
||||
.el-select-group__title {
|
||||
@apply text-purple-500 font-bold;
|
||||
}
|
||||
|
||||
.el-select-dropdown .popper__arrow::after {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ return [
|
|||
'default' => 'Per defecte',
|
||||
'classic' => 'Clàssica',
|
||||
'modern' => 'Moderna',
|
||||
'logo_size_width' => 'Aplada del logotip',
|
||||
'logo_size_height' => 'Alçada del logotip',
|
||||
'hide' => [
|
||||
'item_name' => 'Amaga el nom de producte',
|
||||
'item_description' => 'Amaga la descripció del producte',
|
||||
|
|
|
|||
|
|
@ -48,4 +48,11 @@ return [
|
|||
'description' => 'Previsualització de la versió web de la teva factura que veurà el teu client.',
|
||||
],
|
||||
|
||||
'messages' => [
|
||||
'update_document_transaction' => 'Pots actualitzar aquesta transacció. Ves al document i fes-ho des d\'allà.',
|
||||
'create_document_transaction_error' => 'No es pot afegir aquest enllaç al document. Fes servir {{akaunting_url}}/documents/{{akaunting_document_id}}/transactions',
|
||||
'update_document_transaction_error' => 'No es pot actualitzar aquest enllaç del document. Fes servir {{akaunting_url}}/documents/{{akaunting_document_id}}/transactions/{akaunting_transaction_id}',
|
||||
'delete_document_transaction_error' => 'No es pot esborrar aquest enllaç del document. Fes servir {{akaunting_url}}/documents/{{akaunting_document_id}}/transactions/{akaunting_transaction_id}',
|
||||
]
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ return [
|
|||
'payment_cancel' => 'Warning: You have cancelled your recent :method payment!',
|
||||
'missing_transfer' => 'Warning: The transfer related to this transaction is missing. You should consider deleting this transaction.',
|
||||
'connect_tax' => 'Warning: This :type has a tax amount. Taxes added to the :type can not be connected, so the tax will be added to the total and calculated accordingly.',
|
||||
'contact_change' => 'Warning: You are not allowed to change the contact on a :type that has already been sent, received, or paid!',
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -48,4 +48,11 @@ return [
|
|||
'description' => 'You are previewing how your customer will see the web version of your payment.',
|
||||
],
|
||||
|
||||
'messages' => [
|
||||
'update_document_transaction' => 'You can update this transaction. You should go to the document and edit it there.',
|
||||
'create_document_transaction_error' => 'This endpoint cannot be added to a document. Use {{akaunting_url}}/documents/{{akaunting_document_id}}/transactions',
|
||||
'update_document_transaction_error' => 'This endpoint cannot be updated to a document. Use {{akaunting_url}}/documents/{{akaunting_document_id}}/transactions/{akaunting_transaction_id}',
|
||||
'delete_document_transaction_error' => 'This endpoint cannot be deleted to a document. Use {{akaunting_url}}/documents/{{akaunting_document_id}}/transactions/{akaunting_transaction_id}',
|
||||
]
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ return [
|
|||
'image' => 'The :attribute must be an image.',
|
||||
'in' => 'The selected :attribute is invalid.',
|
||||
'in_array' => 'The :attribute field does not exist in :other.',
|
||||
'in_detailed' => 'The :attribute value ":value" is invalid. Expected one of: :values',
|
||||
'integer' => 'The :attribute must be an integer.',
|
||||
'ip' => 'The :attribute must be a valid IP address.',
|
||||
'ipv4' => 'The :attribute must be a valid IPv4 address.',
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ return [
|
|||
'decimal_mark' => 'Punto decimal',
|
||||
'thousands_separator' => 'Separador de miles',
|
||||
'precision' => 'Precisión',
|
||||
'conversion' => 'Conversión de moneda: :price (:currecy_code) a :currency_rate',
|
||||
'conversion' => 'Conversión de moneda: :price (:currency_code) a :currency_rate',
|
||||
'symbol' => [
|
||||
'symbol' => 'Símbolo',
|
||||
'position' => 'Posición del Símbolo',
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ return [
|
|||
'decimal_mark' => 'Punto decimal',
|
||||
'thousands_separator' => 'Separador de miles',
|
||||
'precision' => 'Precisión',
|
||||
'conversion' => 'Conversión de moneda: :price (:currecy_code) a :currency_rate',
|
||||
'conversion' => 'Conversión de moneda: :price (:currency_code) a :currency_rate',
|
||||
'symbol' => [
|
||||
'symbol' => 'Símbolo',
|
||||
'position' => 'Posición de Símbolo',
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue