Merge branch 'akaunting:master' into master

This commit is contained in:
merve karaman 2023-06-19 09:14:12 +03:00 committed by GitHub
commit 8771f99fda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
171 changed files with 6099 additions and 5007 deletions

View File

@ -20,6 +20,6 @@ abstract class ImportMultipleSheets implements ShouldQueue, WithChunkReading, Wi
public function chunkSize(): int
{
return 100;
return config('excel.imports.chunk_size');
}
}

View File

@ -3,6 +3,7 @@
namespace App\Abstracts\View\Components\Documents;
use App\Abstracts\View\Component;
use App\Interfaces\Utility\DocumentNumber;
use App\Models\Common\Contact;
use App\Models\Document\Document;
use App\Models\Setting\Currency;
@ -815,10 +816,14 @@ abstract class Form extends Component
return $document->document_number;
}
$document_number = $this->getNextDocumentNumber($type);
$contact = ($this->contact instanceof Contact) ? $this->contact : null;
$utility = app(DocumentNumber::class);
$document_number = $utility->getNextNumber($type, $contact);
if (empty($document_number)) {
$document_number = $this->getNextDocumentNumber(Document::INVOICE_TYPE);
$document_number = $utility->getNextNumber(Document::INVOICE_TYPE, $contact);
}
return $document_number;

View File

@ -741,9 +741,7 @@ abstract class Show extends Component
return $textRecurringType;
}
$default_key = config('type.' . static::OBJECT_TYPE . '.' . $type . '.translation.prefix');
$translation = $this->getTextFromConfig($type, 'recurring_tye', $default_key);
$translation = config('type.' . static::OBJECT_TYPE . '.' . $type . '.translation.tab_document');
if (! empty($translation)) {
return $translation;
@ -1254,9 +1252,11 @@ abstract class Show extends Component
return $hideName;
}
$hideName = setting($this->getDocumentSettingKey($type, 'item_name'), false);
// if you use settting translation
if ($hideName = setting($this->getDocumentSettingKey($type, 'item_name'), false) && $hideName == 'hide') {
return $hideName;
if ($hideName === 'hide') {
return true;
}
$hide = $this->getHideFromConfig($type, 'name');
@ -1265,8 +1265,7 @@ abstract class Show extends Component
return $hide;
}
// @todo what return value invoice or always false??
return setting('invoice.item_name', $hideName) == 'hide' ? true : false;
return false;
}
protected function getHideDescription($type, $hideDescription)
@ -1276,8 +1275,8 @@ abstract class Show extends Component
}
// if you use settting translation
if ($hideDescription = setting($this->getDocumentSettingKey($type, 'hide_item_description'), false)) {
return $hideDescription;
if (setting($this->getDocumentSettingKey($type, 'hide_item_description'), false)) {
return true;
}
$hide = $this->getHideFromConfig($type, 'description');
@ -1286,8 +1285,7 @@ abstract class Show extends Component
return $hide;
}
// @todo what return value invoice or always false??
return setting('invoice.hide_item_description', $hideDescription);
return false;
}
protected function getHideQuantity($type, $hideQuantity)
@ -1296,9 +1294,11 @@ abstract class Show extends Component
return $hideQuantity;
}
$hideQuantity = setting($this->getDocumentSettingKey($type, 'quantity_name'), false);
// if you use settting translation
if ($hideQuantity = setting($this->getDocumentSettingKey($type, 'hide_quantity'), false) && $hideQuantity == 'hide') {
return $hideQuantity;
if ($hideQuantity === 'hide') {
return true;
}
$hide = $this->getHideFromConfig($type, 'quantity');
@ -1307,8 +1307,7 @@ abstract class Show extends Component
return $hide;
}
// @todo what return value invoice or always false??
return setting('invoice.quantity_name', $hideQuantity) == 'hide' ? true : false;
return false;
}
protected function getHidePrice($type, $hidePrice)
@ -1317,9 +1316,11 @@ abstract class Show extends Component
return $hidePrice;
}
$hidePrice = setting($this->getDocumentSettingKey($type, 'price_name'), false);
// if you use settting translation
if ($hidePrice = setting($this->getDocumentSettingKey($type, 'hide_price'), false) && $hidePrice == 'hide') {
return $hidePrice;
if ($hidePrice === 'hide') {
return true;
}
$hide = $this->getHideFromConfig($type, 'price');
@ -1328,8 +1329,7 @@ abstract class Show extends Component
return $hide;
}
// @todo what return value invoice or always false??
return setting('invoice.price_name', $hidePrice) == 'hide' ? true : false;
return false;
}
protected function getHideDiscount($type, $hideDiscount)
@ -1360,8 +1360,8 @@ abstract class Show extends Component
}
// if you use settting translation
if ($hideAmount = setting($this->getDocumentSettingKey($type, 'hide_amount'), false)) {
return $hideAmount;
if (setting($this->getDocumentSettingKey($type, 'hide_amount'), false)) {
return true;
}
$hide = $this->getHideFromConfig($type, 'amount');
@ -1370,7 +1370,6 @@ abstract class Show extends Component
return $hide;
}
// @todo what return value invoice or always false??
return setting('invoice.hide_amount', $hideAmount);
return false;
}
}

View File

@ -589,9 +589,11 @@ abstract class Template extends Component
return $hideName;
}
$hideName = setting($this->getDocumentSettingKey($type, 'item_name'), false);
// if you use settting translation
if ($hideName = setting($this->getDocumentSettingKey($type, 'item_name'), false) && $hideName == 'hide') {
return $hideName;
if ($hideName === 'hide') {
return true;
}
$hide = $this->getHideFromConfig($type, 'name');
@ -600,8 +602,7 @@ abstract class Template extends Component
return $hide;
}
// @todo what return value invoice or always false??
return setting('invoice.item_name', $hideName) == 'hide' ? true : false;
return false;
}
protected function getHideDescription($type, $hideDescription)
@ -611,8 +612,8 @@ abstract class Template extends Component
}
// if you use settting translation
if ($hideDescription = setting($this->getDocumentSettingKey($type, 'hide_item_description'), false)) {
return $hideDescription;
if (setting($this->getDocumentSettingKey($type, 'hide_item_description'), false)) {
return true;
}
$hide = $this->getHideFromConfig($type, 'description');
@ -621,8 +622,7 @@ abstract class Template extends Component
return $hide;
}
// @todo what return value invoice or always false??
return setting('invoice.hide_item_description', $hideDescription);
return false;
}
protected function getHideQuantity($type, $hideQuantity)
@ -631,9 +631,11 @@ abstract class Template extends Component
return $hideQuantity;
}
$hideQuantity = setting($this->getDocumentSettingKey($type, 'quantity_name'), false);
// if you use settting translation
if ($hideQuantity = setting($this->getDocumentSettingKey($type, 'hide_quantity'), false) && $hideQuantity == 'hide') {
return $hideQuantity;
if ($hideQuantity === 'hide') {
return true;
}
$hide = $this->getHideFromConfig($type, 'quantity');
@ -642,8 +644,7 @@ abstract class Template extends Component
return $hide;
}
// @todo what return value invoice or always false??
return setting('invoice.quantity_name', $hideQuantity) == 'hide' ? true : false;
return false;
}
protected function getHidePrice($type, $hidePrice)
@ -652,9 +653,11 @@ abstract class Template extends Component
return $hidePrice;
}
$hidePrice = setting($this->getDocumentSettingKey($type, 'price_name'), false);
// if you use settting translation
if ($hidePrice = setting($this->getDocumentSettingKey($type, 'hide_price'), false) && $hidePrice == 'hide') {
return $hidePrice;
if ($hidePrice === 'hide') {
return true;
}
$hide = $this->getHideFromConfig($type, 'price');
@ -663,8 +666,7 @@ abstract class Template extends Component
return $hide;
}
// @todo what return value invoice or always false??
return setting('invoice.price_name', $hidePrice) == 'hide' ? true : false;
return false;
}
protected function getHideDiscount($type, $hideDiscount)
@ -695,8 +697,8 @@ abstract class Template extends Component
}
// if you use settting translation
if ($hideAmount = setting($this->getDocumentSettingKey($type, 'hide_amount'), false)) {
return $hideAmount;
if (setting($this->getDocumentSettingKey($type, 'hide_amount'), false)) {
return true;
}
$hide = $this->getHideFromConfig($type, 'amount');
@ -705,8 +707,7 @@ abstract class Template extends Component
return $hide;
}
// @todo what return value invoice or always false??
return setting('invoice.hide_amount', $hideAmount);
return false;
}
protected function getPrint($print)

View File

@ -1113,9 +1113,7 @@ abstract class Show extends Component
return $textRecurringType;
}
$default_key = config('type.transaction.' . $type . '.translation.transactions');
$translation = $this->getTextFromConfig($type, 'recurring_type', $default_key);
$translation = config('type.transaction.' . $type . '.translation.transactions');
if (! empty($translation)) {
return $translation;

View File

@ -68,6 +68,14 @@ class Update extends Command
$this->old = $this->getOldVersion();
if (version_compare($this->old, $this->new, '>=')) {
$message = 'The current version for the ' . $this->alias . ' is the latest version!';
$this->info($message);
return self::CMD_SUCCESS;
}
company($this->company)->makeCurrent();
if (!$path = $this->download()) {

View File

@ -74,6 +74,14 @@ class Handler extends ExceptionHandler
*/
public function report(Throwable $exception)
{
if ($exception instanceof MailerHttpTransportException) {
$email = $this->handleMailerExceptions($exception);
if (! empty($email)) {
return;
}
}
parent::report($exception);
}
@ -197,23 +205,20 @@ class Handler extends ExceptionHandler
}
if ($exception instanceof MailerHttpTransportException) {
/**
* Couldn't access the SentMessage object to get the email address
* https://symfony.com/doc/current/mailer.html#debugging-emails
*
* https://codespeedy.com/extract-email-addresses-from-a-string-in-php
* https://phpliveregex.com/p/IMG
*/
preg_match("/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}/", $exception->getMessage(), $matches);
$email = $this->handleMailerExceptions($exception);
if (! empty($matches[0])) {
event(new InvalidEmailDetected($matches[0], $exception->getMessage()));
if (! empty($email)) {
$message = trans('notifications.menu.invalid_email.description', ['email' => $email]);
if ($request->ajax()) {
return response()->json([
'error' => trans('notifications.menu.invalid_email.description', ['email' => $matches[0]]),
'error' => $message,
], $exception->getCode());
}
return response()->view('errors.403', [
'message' => $message,
], $exception->getCode());
}
}
@ -237,6 +242,28 @@ class Handler extends ExceptionHandler
return new Response($response, $this->getStatusCode($exception), $this->getHeaders($exception));
}
protected function handleMailerExceptions(MailerHttpTransportException $exception): string
{
/**
* Couldn't access the SentMessage object to get the email address
* https://symfony.com/doc/current/mailer.html#debugging-emails
*
* https://codespeedy.com/extract-email-addresses-from-a-string-in-php
* https://phpliveregex.com/p/IMG
*/
preg_match("/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}/", $exception->getMessage(), $matches);
if (empty($matches[0])) {
return '';
}
$email = $matches[0];
event(new InvalidEmailDetected($email, $exception->getMessage()));
return $email;
}
/**
* Prepare the replacements array by gathering the keys and values.
*

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Auth;
use App\Abstracts\Http\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request as BaseRequest;
use App\Http\Requests\Auth\Reset as Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
@ -29,7 +30,7 @@ class Reset extends Controller
$this->middleware('guest');
}
public function create(Request $request, $token = null)
public function create(BaseRequest $request, $token = null)
{
return view('auth.reset.create')->with(
['token' => $token, 'email' => $request->email]

View File

@ -47,9 +47,19 @@ class Users extends Controller
*
* @return Response
*/
public function show()
public function show(User $user)
{
return redirect()->route('users.index');
$u = new \stdClass();
$u->role = $user->roles()->first();
$u->landing_pages = [];
event(new LandingPageShowing($u));
$landing_pages = $u->landing_pages;
$companies = $user->companies()->collect();
return view('auth.users.show', compact('user', 'landing_pages', 'companies'));
}
/**
@ -67,7 +77,13 @@ class Users extends Controller
$landing_pages = $u->landing_pages;
$roles = Role::all()->reject(function ($r) {
return $r->hasPermission('read-client-portal');
$status = $r->hasPermission('read-client-portal');
if ($r->name == 'employee') {
$status = true;
}
return $status;
})->pluck('display_name', 'id');
$companies = user()->companies()->take(setting('default.select_limit'))->get()->sortBy('name')->pluck('name', 'id');
@ -89,7 +105,7 @@ class Users extends Controller
$response = $this->ajaxDispatch(new CreateUser($request));
if ($response['success']) {
$response['redirect'] = route('users.index');
$response['redirect'] = route('users.show', $response['data']->id);
$message = trans('messages.success.invited', ['type' => trans_choice('general.users', 1)]);
@ -129,12 +145,21 @@ class Users extends Controller
if ($user->isCustomer()) {
// Show only roles with customer permission
$roles = Role::all()->reject(function ($r) {
return !$r->hasPermission('read-client-portal');
return ! $r->hasPermission('read-client-portal');
})->pluck('display_name', 'id');
} else if ($user->isEmployee()) {
// Show only roles with employee permission
$roles = Role::where('name', 'employee')->get()->pluck('display_name', 'id');
} else {
// Don't show roles with customer permission
$roles = Role::all()->reject(function ($r) {
return $r->hasPermission('read-client-portal');
$status = $r->hasPermission('read-client-portal');
if ($r->name == 'employee') {
$status = true;
}
return $status;
})->pluck('display_name', 'id');
}
@ -176,7 +201,7 @@ class Users extends Controller
$response = $this->ajaxDispatch(new UpdateUser($user, $request));
if ($response['success']) {
$response['redirect'] = user()->can('read-auth-users') ? route('users.index') : route('users.edit', $user->id);
$response['redirect'] = user()->can('read-auth-users') ? route('users.show', $user->id) : route('users.edit', $user->id);
$message = trans('messages.success.updated', ['type' => $user->name]);

View File

@ -35,7 +35,7 @@ class Accounts extends Controller
*/
public function show(Account $account)
{
$transactions = Transaction::with('category', 'contact', 'document')->where('account_id', $account->id)->collect(['paid_at'=> 'desc']);
$transactions = Transaction::with('category', 'contact', 'contact.media', 'document', 'document.totals', 'document.media', 'recurring', 'media')->where('account_id', $account->id)->collect(['paid_at'=> 'desc']);
$transfers = Transfer::with('expense_transaction', 'expense_transaction.account', 'income_transaction', 'income_transaction.account')
->whereHas('expense_transaction', fn ($query) => $query->where('account_id', $account->id))

View File

@ -63,9 +63,9 @@ class RecurringTransactions extends Controller
*/
public function create()
{
$type = request()->get('type', 'income-recurring');
$real_type = request()->get('real_type', $this->getRealTypeOfRecurringTransaction($type));
$contact_type = config('type.transaction.' . $real_type . '.contact_type');
$type = $this->getTypeRecurringTransaction(request()->get('type', 'income-recurring'));
$real_type = $this->getTypeTransaction(request()->get('real_type', $this->getRealTypeOfRecurringTransaction($type)));
$contact_type = config('type.transaction.' . $real_type . '.contact_type', 'customer');
$number = $this->getNextTransactionNumber('-recurring');
@ -139,8 +139,8 @@ class RecurringTransactions extends Controller
public function edit(Transaction $recurring_transaction)
{
$type = $recurring_transaction->type;
$real_type = request()->get('real_type', $this->getRealTypeOfRecurringTransaction($type));
$contact_type = config('type.transaction.' . $real_type . '.contact_type');
$real_type = $this->getTypeTransaction(request()->get('real_type', $this->getRealTypeOfRecurringTransaction($type)));
$contact_type = config('type.transaction.' . $real_type . '.contact_type', 'customer');
$number = $this->getNextTransactionNumber('-recurring');

View File

@ -98,10 +98,10 @@ class Transactions extends Controller
*/
public function create()
{
$type = request()->get('type', 'income');
$type = $this->getTypeTransaction(request()->get('type', 'income'));
$real_type = $this->getRealTypeTransaction($type);
$number = $this->getNextTransactionNumber();
$number = $this->getNextTransactionNumber($type);
$contact_type = config('type.transaction.' . $type . '.contact_type');

View File

@ -16,6 +16,15 @@ class Companies extends Controller
{
use Uploads, Users;
public function __construct()
{
// Add CRUD permission checks to all methods only remove index method for all companies list.
$this->middleware('permission:create-common-companies')->only('create', 'store', 'duplicate', 'import');
$this->middleware('permission:read-common-companies')->only('show', 'edit', 'export');
$this->middleware('permission:update-common-companies')->only('update', 'enable', 'disable');
$this->middleware('permission:delete-common-companies')->only('destroy');
}
/**
* Display a listing of the resource.
*

View File

@ -34,6 +34,22 @@ class Uploads extends Controller
return $this->streamMedia($media);
}
public function inline($id)
{
try {
$media = Media::find($id);
} catch (\Exception $e) {
return response(null, 204);
}
// Get file path
if (!$this->getMediaPathOnStorage($media)) {
return response(null, 204);
}
return $this->streamMedia($media, 'inline');
}
/**
* Get the specified resource.
*

View File

@ -242,16 +242,30 @@ class Item extends Controller
$this->dispatch(new InstallModule($request['alias'], company_id()));
$name = module($request['alias'])->getName();
$module_routes = module_attribute($request['alias'], 'routes', []);
$message = trans('modules.installed', ['module' => $name]);
flash($message)->success();
$redirect = route('apps.app.show', $request['alias']);
// Get module.json redirect route
if (! empty($module_routes['redirect_after_install'])) {
if (is_array($module_routes['redirect_after_install'])) {
$route = array_shift($module_routes['redirect_after_install']);
$redirect = route($route, $module_routes['redirect_after_install']);
} else {
$redirect = route($module_routes['redirect_after_install']);
}
}
$json = [
'success' => true,
'error' => false,
'message' => null,
'redirect' => route('apps.app.show', $request['alias']),
'redirect' => $redirect,
'data' => [
'name' => $name,
'alias' => $request['alias'],

View File

@ -7,7 +7,6 @@ use App\Exports\Purchases\Bills as Export;
use App\Http\Requests\Common\Import as ImportRequest;
use App\Http\Requests\Document\Document as Request;
use App\Imports\Purchases\Bills as Import;
use App\Jobs\Banking\CreateBankingDocumentTransaction;
use App\Jobs\Document\CreateDocument;
use App\Jobs\Document\DeleteDocument;
use App\Jobs\Document\DuplicateDocument;
@ -31,7 +30,7 @@ class Bills extends Controller
*/
public function index()
{
$bills = Document::bill()->with('contact', 'items', 'last_history', 'transactions')->collect(['issued_at' => 'desc']);
$bills = Document::bill()->with('contact', 'items', 'item_taxes', 'last_history', 'transactions', 'totals', 'histories', 'media')->collect(['issued_at' => 'desc']);
return $this->response('purchases.bills.index', compact('bills'));
}

View File

@ -13,7 +13,6 @@ use App\Jobs\Document\DuplicateDocument;
use App\Jobs\Document\SendDocument;
use App\Jobs\Document\UpdateDocument;
use App\Models\Document\Document;
use App\Notifications\Sale\Invoice as Notification;
use App\Traits\Documents;
class Invoices extends Controller
@ -32,7 +31,7 @@ class Invoices extends Controller
*/
public function index()
{
$invoices = Document::invoice()->with('contact', 'items', 'last_history', 'transactions')->collect(['document_number'=> 'desc']);
$invoices = Document::invoice()->with('contact', 'items', 'item_taxes', 'last_history', 'transactions', 'totals', 'histories', 'media')->collect(['document_number'=> 'desc']);
return $this->response('sales.invoices.index', compact('invoices'));
}

View File

@ -17,6 +17,7 @@ class CustomMail extends FormRequest
'to' => 'required|email',
'subject' => 'required|string',
'body' => 'required|string',
'attachments.*' => 'nullable|boolean',
];
}
}

View File

@ -23,9 +23,9 @@ class Item extends JsonResource
'name' => $this->name,
'description' => $this->description,
'sale_price' => $this->sale_price,
'sale_price_formatted' => money($this->sale_price, default_currency(), true)->format(),
'sale_price_formatted' => money((double) $this->sale_price, default_currency(), true)->format(),
'purchase_price' => $this->purchase_price,
'purchase_price_formatted' => money($this->purchase_price, default_currency(), true)->format(),
'purchase_price_formatted' => money((double) $this->purchase_price, default_currency(), true)->format(),
'category_id' => $this->category_id,
'picture' => $this->picture,
'enabled' => $this->enabled,

View File

@ -8,6 +8,7 @@ use App\Http\Resources\Document\DocumentHistory;
use App\Http\Resources\Document\DocumentItem;
use App\Http\Resources\Document\DocumentItemTax;
use App\Http\Resources\Document\DocumentTotal;
use App\Http\Resources\Setting\Category;
use App\Http\Resources\Setting\Currency;
use Illuminate\Http\Resources\Json\JsonResource;
@ -32,6 +33,7 @@ class Document extends JsonResource
'due_at' => $this->due_at ? $this->due_at->toIso8601String() : '',
'amount' => $this->amount,
'amount_formatted' => money($this->amount, $this->currency_code, true)->format(),
'category_id' => $this->category_id,
'currency_code' => $this->currency_code,
'currency_rate' => $this->currency_rate,
'contact_id' => $this->contact_id,
@ -50,6 +52,7 @@ class Document extends JsonResource
'created_by' => $this->created_by,
'created_at' => $this->created_at ? $this->created_at->toIso8601String() : '',
'updated_at' => $this->updated_at ? $this->updated_at->toIso8601String() : '',
'category' => new Category($this->category),
'currency' => new Currency($this->currency),
'contact' => new Contact($this->contact),
'histories' => [static::$wrap => DocumentHistory::collection($this->histories)],

View File

@ -0,0 +1,12 @@
<?php
namespace App\Interfaces\Utility;
use App\Models\Common\Contact;
interface DocumentNumber
{
public function getNextNumber(string $type, ?Contact $contact): string;
public function increaseNextNumber(string $type, ?Contact $contact): void;
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Interfaces\Utility;
use App\Models\Common\Contact;
interface TransactionNumber
{
public function getNextNumber(string $type, string $suffix, ?Contact $contact): string;
public function increaseNextNumber(string $type, string $suffix, ?Contact $contact): void;
}

View File

@ -23,6 +23,12 @@ class CreateInvitation extends Job
public function handle(): UserInvitation
{
\DB::transaction(function () {
$invitations = UserInvitation::where('user_id', $this->user->id)->get();
foreach ($invitations as $invitation) {
$invitation->delete();
}
$this->invitation = UserInvitation::create([
'user_id' => $this->user->id,
'token' => (string) Str::uuid(),

View File

@ -12,6 +12,10 @@ class UpdateRole extends Job implements ShouldUpdate
{
public function handle(): Role
{
if (in_array($this->model->name, config('roles.defaults', ['admin', 'manager', 'accountant', 'employee']))) {
$this->request->name = $this->model->name;
}
event(new RoleUpdating($this->model, $this->request));
\DB::transaction(function () {

View File

@ -16,6 +16,12 @@ class CreateTransaction extends Job implements HasOwner, HasSource, ShouldCreate
{
event(new TransactionCreating($this->request));
if (! array_key_exists($this->request->get('type'), config('type.transaction'))) {
$type = (empty($this->request->get('recurring_frequency')) || ($this->request->get('recurring_frequency') == 'no')) ? Transaction::INCOME_TYPE : Transaction::INCOME_RECURRING_TYPE;
$this->request->merge(['type' => $type]);
}
\DB::transaction(function () {
$this->model = Transaction::create($this->request->all());

View File

@ -34,6 +34,7 @@ class DeleteAccount extends Job implements ShouldDelete
{
$rels = [
'transactions' => 'transactions',
'reconciliations' => 'reconciliations',
];
$relationships = $this->countRelationships($this->model, $rels);

View File

@ -16,6 +16,12 @@ class UpdateTransaction extends Job implements ShouldUpdate
event(new TransactionUpdating($this->model, $this->request));
if (! array_key_exists($this->request->get('type'), config('type.transaction'))) {
$type = (empty($this->request->get('recurring_frequency')) || ($this->request->get('recurring_frequency') == 'no')) ? Transaction::INCOME_TYPE : Transaction::INCOME_RECURRING_TYPE;
$this->request->merge(['type' => $type]);
}
\DB::transaction(function () {
$this->model->update($this->request->all());

View File

@ -30,10 +30,15 @@ class SendDocumentAsCustomMail extends Job
$custom_mail['cc'] = user()->email;
}
$attachments = collect($this->request->get('attachments', []))
->filter(fn($value) => $value == true)
->keys()
->all();
$notification = config('type.document.' . $document->type . '.notification.class');
// Notify the contact
$document->contact->notify(new $notification($document, $this->template_alias, true, $custom_mail));
$document->contact->notify(new $notification($document, $this->template_alias, true, $custom_mail, $attachments));
event(new DocumentSent($document));
}

View File

@ -22,6 +22,10 @@ class SendDocumentPaymentNotification
$document = $event->document;
$transaction = $document->transactions()->latest()->first();
if (! $transaction) {
return;
}
// Notify the customer
if ($document->contact && !empty($document->contact_email)) {
$document->contact->notify(new Notification($document, $transaction, "{$document->type}_payment_customer"), true);

View File

@ -29,6 +29,13 @@ class DisablePersonDueToInvalidEmail
return;
}
// If only one user is left, don't disable it
$users = company()?->users;
if ($users && $users->count() <= 1) {
return;
}
$event->user->enabled = false;
$event->user->save();
}

View File

@ -10,7 +10,11 @@ class SendInvalidEmailNotification
{
public function handle(Event $event): void
{
$users = company()->users;
$users = company()?->users;
if (empty($users)) {
return;
}
$this->notifyAdminsAboutInvalidContactEmail($event, $users);
@ -44,7 +48,7 @@ class SendInvalidEmailNotification
return;
}
$type = trans('general.users', 1);
$type = trans_choice('general.users', 1);
foreach ($users as $user) {
if ($user->cannot('read-notifications')) {

View File

@ -30,12 +30,16 @@ class TellFirewallTooManyEmailsSent
public function loadConfig(): void
{
if (! empty(Config::get('firewall.middleware.' . $this->middleware))) {
return;
}
$config = array_merge_recursive(
Config::get('firewall'),
[
'middleware' => [
$this->middleware => [
'enabled' => env('FIREWALL_MIDDLEWARE_' . strtoupper($this->middleware) . '_ENABLED', env('FIREWALL_ENABLED', true)),
'enabled' => env('FIREWALL_MIDDLEWARE_' . strtoupper($this->middleware) . '_ENABLED', Config::get('firewall.enabled', true)),
'methods' => ['post'],

View File

@ -0,0 +1,43 @@
<?php
namespace App\Listeners\Update\V30;
use App\Abstracts\Listeners\Update as Listener;
use App\Events\Install\UpdateFinished as Event;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;
class Version3015 extends Listener
{
const ALIAS = 'core';
const VERSION = '3.0.15';
/**
* Handle the event.
*
* @param $event
* @return void
*/
public function handle(Event $event)
{
if ($this->skipThisUpdate($event)) {
return;
}
Log::channel('stdout')->info('Updating to 3.0.15 version...');
$this->updateDatabase();
Log::channel('stdout')->info('Done!');
}
public function updateDatabase(): void
{
Log::channel('stdout')->info('Updating database...');
Artisan::call('migrate', ['--force' => true]);
Log::channel('stdout')->info('Database updated.');
}
}

View File

@ -29,6 +29,35 @@ class Role extends LaratrustRole
*/
public $cloneable_relations = ['permissions'];
/**
* Scope to get all rows filtered, sorted and paginated.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param $sort
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeCollect($query, $sort = 'display_name')
{
$request = request();
$search = $request->get('search');
$limit = (int) $request->get('limit', setting('default.list_limit', '25'));
return $query->usingSearchString($search)->sortable($sort)->paginate($limit);
}
/**
* @inheritDoc
*
* @param Document $src
* @param boolean $child
*/
public function onCloning($src, $child = null)
{
$this->name = $src->name . '-' . Role::max('id') + 1;
}
/**
* Get the line actions.
*
@ -71,33 +100,4 @@ class Role extends LaratrustRole
return $actions;
}
/**
* Scope to get all rows filtered, sorted and paginated.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param $sort
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeCollect($query, $sort = 'display_name')
{
$request = request();
$search = $request->get('search');
$limit = (int) $request->get('limit', setting('default.list_limit', '25'));
return $query->usingSearchString($search)->sortable($sort)->paginate($limit);
}
/**
* @inheritDoc
*
* @param Document $src
* @param boolean $child
*/
public function onCloning($src, $child = null)
{
$this->name = $src->name . '-' . Role::max('id') + 1;
}
}

View File

@ -38,6 +38,9 @@ class User extends Authenticatable implements HasLocalePreference
*/
protected $casts = [
'enabled' => 'boolean',
'last_logged_in_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
@ -48,13 +51,6 @@ class User extends Authenticatable implements HasLocalePreference
*/
protected $hidden = ['password', 'remember_token'];
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['last_logged_in_at', 'created_at', 'updated_at', 'deleted_at'];
/**
* Sortable columns.
*
@ -244,6 +240,28 @@ class User extends Authenticatable implements HasLocalePreference
return $query->wherePermissionIs('read-admin-panel');
}
/**
* Scope to only employees.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeIsEmployee($query)
{
return $query->whereHasRole('employee');
}
/**
* Scope to only users.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeIsNotEmployee($query)
{
return $query->wherePermissionIs('read-admin-panel');
}
public function scopeEmail($query, $email)
{
return $query->where('email', '=', $email);
@ -293,6 +311,26 @@ class User extends Authenticatable implements HasLocalePreference
return (bool) $this->can('read-admin-panel');
}
/**
* Determine if user is a employee.
*
* @return bool
*/
public function isEmployee()
{
return (bool) $this->hasRole('employee');
}
/**
* Determine if user is not a employee.
*
* @return bool
*/
public function isNotEmployee()
{
return (bool) ! $this->hasRole('employee');
}
public function scopeSource($query, $source)
{
return $query->where($this->qualifyColumn('created_from'), $source);
@ -340,13 +378,23 @@ class User extends Authenticatable implements HasLocalePreference
return $actions;
}
$actions[] = [
'title' => trans('general.show'),
'icon' => 'visibility',
'url' => route('users.show', $this->id),
'permission' => 'read-auth-users',
'attributes' => [
'id' => 'index-line-actions-show-user-' . $this->id,
],
];
$actions[] = [
'title' => trans('general.edit'),
'icon' => 'edit',
'url' => route('users.edit', $this->id),
'permission' => 'update-auth-users',
'attributes' => [
'id' => 'index-line-actions-show-user-' . $this->id,
'id' => 'index-line-actions-edit-user-' . $this->id,
],
];

View File

@ -65,6 +65,11 @@ class Account extends Model
return $this->hasMany('App\Models\Banking\Transaction');
}
public function reconciliations()
{
return $this->hasMany('App\Models\Banking\Reconciliation');
}
public function scopeName($query, $name)
{
return $query->where('name', '=', $name);

View File

@ -11,8 +11,6 @@ class Reconciliation extends Model
protected $table = 'reconciliations';
protected $dates = ['deleted_at', 'started_at', 'ended_at'];
/**
* Attributes that should be mass-assignable.
*
@ -30,6 +28,8 @@ class Reconciliation extends Model
'reconciled' => 'boolean',
'transactions' => 'array',
'deleted_at' => 'datetime',
'started_at' => 'datetime',
'ended_at' => 'datetime',
];
/**
@ -41,7 +41,7 @@ class Reconciliation extends Model
public function account()
{
return $this->belongsTo('App\Models\Banking\Account');
return $this->belongsTo('App\Models\Banking\Account')->withDefault(['name' => trans('general.na')]);
}
/**

View File

@ -304,6 +304,7 @@ class Transaction extends Model
$this->number = $this->getNextTransactionNumber($suffix);
$this->document_id = null;
$this->split_id = null;
unset($this->reconciled);
}
/**

View File

@ -27,6 +27,8 @@ class Company extends Eloquent implements Ownable
protected $table = 'companies';
//protected $with = ['settings'];
/**
* The accessors to append to the model's array form.
*
@ -527,7 +529,7 @@ class Company extends Eloquent implements Ownable
$country = setting('company.country');
if ($country && in_array($country, trans('countries'))) {
if ($country && array_key_exists($country, trans('countries'))) {
$location[] = trans('countries.' . $country);
}
@ -548,7 +550,7 @@ class Company extends Eloquent implements Ownable
'title' => trans('general.switch'),
'icon' => 'settings_ethernet',
'url' => route('companies.switch', $this->id),
'permission' => 'read-common-companies',
//'permission' => 'read-common-companies', remove this permission to allow switching to any company
'attributes' => [
'id' => 'index-line-actions-switch-company-' . $this->id,
],

View File

@ -26,6 +26,13 @@ class Contact extends Model
protected $table = 'contacts';
/**
* The relationships that should always be loaded.
*
* @var array
*/
protected $with = ['media'];
/**
* The accessors to append to the model's array form.
*
@ -171,6 +178,17 @@ class Contact extends Model
return $query->whereIn($this->qualifyColumn('type'), (array) $this->getCustomerTypes());
}
/**
* Scope to include only employees.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeEmployee($query)
{
return $query->whereIn($this->qualifyColumn('type'), (array) $this->getEmployeeTypes());
}
public function scopeEmail($query, $email)
{
return $query->where('email', '=', $email);
@ -260,7 +278,7 @@ class Contact extends Model
$location[] = $this->state;
}
if ($this->country && in_array($this->country, trans('countries'))) {
if ($this->country && array_key_exists($this->country, trans('countries'))) {
$location[] = trans('countries.' . $this->country);
}

View File

@ -16,6 +16,13 @@ class Item extends Model
protected $table = 'items';
/**
* The relationships that should always be loaded.
*
* @var array
*/
protected $with = ['taxes'];
/**
* The accessors to append to the model's array form.
*
@ -94,6 +101,15 @@ class Item extends Model
return $query->whereNotNull($price_type . '_price');
}
public function scopeType($query, $type)
{
if (empty($type)) {
return $query;
}
return $query->where($this->qualifyColumn('type'), $type);
}
/**
* Get the item id.
*

View File

@ -3,6 +3,7 @@
namespace App\Models\Document;
use App\Abstracts\Model;
use App\Interfaces\Utility\DocumentNumber;
use App\Models\Common\Media as MediaModel;
use App\Models\Setting\Tax;
use App\Scopes\Document as Scope;
@ -251,7 +252,7 @@ class Document extends Model
}
$this->status = 'draft';
$this->document_number = $this->getNextDocumentNumber($type);
$this->document_number = app(DocumentNumber::class)->getNextNumber($type, $src->contact);
}
public function getSentAtAttribute(string $value = null)
@ -470,7 +471,7 @@ class Document extends Model
$location[] = $this->contact_state;
}
if ($this->contact_country && in_array($this->contact_country, trans('countries'))) {
if ($this->contact_country && array_key_exists($this->contact_country, trans('countries'))) {
$location[] = trans('countries.' . $this->contact_country);
}

View File

@ -15,6 +15,13 @@ class DocumentItem extends Model
protected $table = 'document_items';
/**
* The relationships that should always be loaded.
*
* @var array
*/
protected $with = ['taxes'];
protected $appends = ['discount'];
protected $fillable = [

View File

@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\HtmlString;
class ExportCompleted extends Notification implements ShouldQueue
{
@ -52,6 +53,7 @@ class ExportCompleted extends Notification implements ShouldQueue
{
return (new MailMessage)
->subject(trans('notifications.export.completed.title'))
->line(new HtmlString('<br><br>'))
->line(trans('notifications.export.completed.description'))
->action(trans('general.download'), $this->download_url);
}

View File

@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\HtmlString;
class ExportFailed extends Notification implements ShouldQueue
{
@ -51,8 +52,11 @@ class ExportFailed extends Notification implements ShouldQueue
{
return (new MailMessage)
->subject(trans('notifications.export.failed.title'))
->line(new HtmlString('<br><br>'))
->line(trans('notifications.export.failed.description'))
->line($this->message);
->line(new HtmlString('<br><br>'))
->line($this->message)
->line(new HtmlString('<br><br>'));
}
/**

View File

@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\HtmlString;
class ImportCompleted extends Notification implements ShouldQueue
{
@ -49,6 +50,7 @@ class ImportCompleted extends Notification implements ShouldQueue
return (new MailMessage)
->subject(trans('notifications.import.completed.title'))
->line(new HtmlString('<br><br>'))
->line(trans('notifications.import.completed.description'))
->action(trans_choice('general.dashboards', 1), $dashboard_url);
}

View File

@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\HtmlString;
class ImportFailed extends Notification implements ShouldQueue
{
@ -51,12 +52,16 @@ class ImportFailed extends Notification implements ShouldQueue
{
$message = (new MailMessage)
->subject(trans('notifications.import.failed.title'))
->line(new HtmlString('<br><br>'))
->line(trans('notifications.import.failed.description'));
foreach ($this->errors as $error) {
$message->line(new HtmlString('<br><br>'));
$message->line($error);
}
$message->line(new HtmlString('<br><br>'));
return $message;
}

View File

@ -55,11 +55,9 @@ class InvalidEmail extends Notification implements ShouldQueue
return (new MailMessage)
->subject(trans('notifications.email.invalid.title', ['type' => $this->type]))
->line(new HtmlString('<br>'))
->line(new HtmlString('<br>'))
->line(new HtmlString('<br><br>'))
->line(trans('notifications.email.invalid.description', ['email' => $this->email]))
->line(new HtmlString('<br>'))
->line(new HtmlString('<br>'))
->line(new HtmlString('<br><br>'))
->line(new HtmlString('<i>' . $this->error . '</i>'))
->action(trans_choice('general.dashboards', 1), $dashboard_url);
}

View File

@ -35,10 +35,17 @@ class Invoice extends Notification
*/
public $attach_pdf;
/**
* List of document attachments to attach when sending the email.
*
* @var array
*/
public $attachments;
/**
* Create a notification instance.
*/
public function __construct(Document $invoice = null, string $template_alias = null, bool $attach_pdf = false, array $custom_mail = [])
public function __construct(Document $invoice = null, string $template_alias = null, bool $attach_pdf = false, array $custom_mail = [], $attachments = [])
{
parent::__construct();
@ -46,6 +53,7 @@ class Invoice extends Notification
$this->template = EmailTemplate::alias($template_alias)->first();
$this->attach_pdf = $attach_pdf;
$this->custom_mail = $custom_mail;
$this->attachments = $attachments;
}
/**
@ -68,6 +76,17 @@ class Invoice extends Notification
]);
}
// Attach selected attachments
if (! empty($this->invoice->attachment)) {
foreach ($this->invoice->attachment as $attachment) {
if (in_array($attachment->id, $this->attachments)) {
$message->attach($attachment->getAbsolutePath(), [
'mime' => $attachment->mime_type,
]);
}
}
}
return $message;
}

View File

@ -43,9 +43,13 @@ class App extends Provider
Model::preventLazyLoading(config('app.eager_load'));
Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
if (config('logging.default') == 'sentry') {
\Sentry\Laravel\Integration::lazyLoadingViolationReporter();
} else {
$class = get_class($model);
report("Attempted to lazy load [{$relation}] on model [{$class}].");
}
});
}
}

22
app/Providers/Binding.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace App\Providers;
use App\Interfaces\Utility\DocumentNumber as DocumentNumberInterface;
use App\Interfaces\Utility\TransactionNumber as TransactionNumberInterface;
use App\Utilities\DocumentNumber;
use App\Utilities\TransactionNumber;
use Illuminate\Support\ServiceProvider;
class Binding extends ServiceProvider
{
/**
* All container bindings that should be registered.
*
* @var array
*/
public array $bindings = [
DocumentNumberInterface::class => DocumentNumber::class,
TransactionNumberInterface::class => TransactionNumber::class,
];
}

View File

@ -29,6 +29,10 @@ class Blade extends ServiceProvider
return "<?php echo show_widget($expression); ?>";
});
Facade::directive('moduleIsEnabled', function ($expression) {
return "<?php echo module_is_enabled($expression); ?>";
});
Facade::if('readonly', function () {
return config('read-only.enabled');
});

View File

@ -23,6 +23,7 @@ class Event extends Provider
'App\Listeners\Update\V30\Version309',
'App\Listeners\Update\V30\Version3013',
'App\Listeners\Update\V30\Version3014',
'App\Listeners\Update\V30\Version3015',
],
'Illuminate\Auth\Events\Login' => [
'App\Listeners\Auth\Login',

View File

@ -2,6 +2,7 @@
namespace App\Traits;
use App\Interfaces\Utility\DocumentNumber;
use App\Models\Document\Document;
use App\Abstracts\View\Components\Documents\Document as DocumentComponent;
use App\Utilities\Date;
@ -44,29 +45,24 @@ trait Documents
return $recurring_types;
}
/**
* Deprecated. Use the DocumentNumber::getNextNumber() method instead.
*
* @deprecated This method is deprecated and will be removed in future versions.
*/
public function getNextDocumentNumber(string $type): string
{
if ($alias = config('type.document.' . $type . '.alias')) {
$type = $alias . '.' . str_replace('-', '_', $type);
}
$prefix = setting($type . '.number_prefix');
$next = (string) setting($type . '.number_next');
$digit = (int) setting($type . '.number_digit');
return $prefix . str_pad($next, $digit, '0', STR_PAD_LEFT);
return app(DocumentNumber::class)->getNextNumber($type, null);
}
/**
* Deprecated. Use the DocumentNumber::increaseNextNumber() method instead.
*
* @deprecated This method is deprecated and will be removed in future versions.
*/
public function increaseNextDocumentNumber(string $type): void
{
if ($alias = config('type.document.' . $type . '.alias')) {
$type = $alias . '.' . str_replace('-', '_', $type);
}
$next = setting($type . '.number_next', 1) + 1;
setting([$type . '.number_next' => $next]);
setting()->save();
app(DocumentNumber::class)->increaseNextNumber($type, null);
}
public function getDocumentStatuses(string $type): Collection

View File

@ -137,12 +137,14 @@ trait Import
return is_null($id) ? $id : (int) $id;
}
public function getItemId($row)
public function getItemId($row, $type = null)
{
$id = isset($row['item_id']) ? $row['item_id'] : null;
$type = !empty($type) ? $type : (!empty($row['item_type']) ? $row['item_type'] : 'product');
if (empty($id) && !empty($row['item_name'])) {
$id = $this->getItemIdFromName($row);
$id = $this->getItemIdFromName($row, $type);
}
return is_null($id) ? $id : (int) $id;
@ -246,7 +248,7 @@ trait Import
public function getCategoryIdFromName($row, $type)
{
$category_id = Category::withSubCategory()->where('name', $row['category_name'])->pluck('id')->first();
$category_id = Category::type($type)->withSubCategory()->where('name', $row['category_name'])->pluck('id')->first();
if (!empty($category_id)) {
return $category_id;
@ -271,7 +273,7 @@ trait Import
public function getContactIdFromEmail($row, $type)
{
$contact_id = Contact::where('email', $row['contact_email'])->pluck('id')->first();
$contact_id = Contact::type($type)->where('email', $row['contact_email'])->pluck('id')->first();
if (!empty($contact_id)) {
return $contact_id;
@ -297,7 +299,7 @@ trait Import
public function getContactIdFromName($row, $type)
{
$contact_id = Contact::where('name', $row['contact_name'])->pluck('id')->first();
$contact_id = Contact::type($type)->where('name', $row['contact_name'])->pluck('id')->first();
if (!empty($contact_id)) {
return $contact_id;
@ -321,9 +323,11 @@ trait Import
return $contact->id;
}
public function getItemIdFromName($row)
public function getItemIdFromName($row, $type = null)
{
$item_id = Item::where('name', $row['item_name'])->pluck('id')->first();
$type = !empty($type) ? $type : (!empty($row['item_type']) ? $row['item_type'] : 'product');
$item_id = Item::type($type)->where('name', $row['item_name'])->pluck('id')->first();
if (!empty($item_id)) {
return $item_id;
@ -331,7 +335,7 @@ trait Import
$data = [
'company_id' => company_id(),
'type' => !empty($row['item_type']) ? $row['item_type'] : (!empty($row['type']) ? $row['type'] : 'product'),
'type' => $type,
'name' => $row['item_name'],
'description' => !empty($row['item_description']) ? $row['item_description'] : null,
'sale_price' => !empty($row['sale_price']) ? $row['sale_price'] : (!empty($row['price']) ? $row['price'] : 0),
@ -350,7 +354,7 @@ trait Import
public function getTaxIdFromRate($row, $type = 'normal')
{
$tax_id = Tax::where('rate', $row['tax_rate'])->pluck('id')->first();
$tax_id = Tax::type($type)->where('rate', $row['tax_rate'])->pluck('id')->first();
if (!empty($tax_id)) {
return $tax_id;

View File

@ -440,7 +440,7 @@ trait Modules
}
// Check if module is installed in cloud
if (request()->getHost() == 'app.akaunting.com') {
if (request()->getHost() == 'app.akaunting.com' || request()->getHost() == 'localhost') {
$modules = Cache::get('cloud.companies.' . company_id() . '.modules.installed', []);
if (in_array($alias, $modules)) {

View File

@ -186,26 +186,19 @@ trait Recurring
return $limit;
}
public function getCurrentRecurring()
{
if (! $schedule = $this->getRecurringSchedule()) {
return false;
}
if (! $current = $schedule->current()) {
return false;
}
return $current->getStart();
}
public function getNextRecurring()
{
if (! $schedule = $this->getRecurringSchedule()) {
return false;
}
if (! $next = $schedule->next()) {
$schedule = $schedule->startsAfter($this->getRecurringRuleTodayDate());
if ($schedule->count() == 0) {
return false;
}
if (! $next = $schedule->current()) {
return false;
}

View File

@ -4,6 +4,7 @@ namespace App\Traits;
use App\Events\Banking\TransactionPrinting;
use App\Models\Banking\Transaction;
use App\Interfaces\Utility\TransactionNumber;
use Illuminate\Support\Str;
trait Transactions
@ -204,6 +205,11 @@ trait Transactions
];
}
public function getTypeTransaction(string $type = Transaction::INCOME_TYPE): string
{
return array_key_exists($type, config('type.transaction')) ? $type : Transaction::INCOME_TYPE;
}
public function getRealTypeTransaction(string $type): string
{
$type = $this->getRealTypeOfRecurringTransaction($type);
@ -213,6 +219,15 @@ trait Transactions
return $type;
}
public function getTypeRecurringTransaction(string $type = Transaction::INCOME_RECURRING_TYPE): string
{
if (! Str::contains($type, '-recurring')) {
return Transaction::INCOME_RECURRING_TYPE;
}
return array_key_exists($type, config('type.transaction')) ? $type : Transaction::INCOME_RECURRING_TYPE;
}
public function getRealTypeOfRecurringTransaction(string $recurring_type): string
{
return Str::replace('-recurring', '', $recurring_type);
@ -228,36 +243,13 @@ trait Transactions
return Str::replace('-split', '', $transfer_type);
}
public function getNextTransactionNumber($suffix = ''): string
public function getNextTransactionNumber($type = 'income', $suffix = ''): string
{
$prefix = setting('transaction' . $suffix . '.number_prefix');
$next = (string) setting('transaction' . $suffix . '.number_next');
$digit = (int) setting('transaction' . $suffix . '.number_digit');
$get_number = fn($prefix, $next, $digit) => $prefix . str_pad($next, $digit, '0', STR_PAD_LEFT);
$number_exists = fn($number) => Transaction::where('number', $number)->exists();
$transaction_number = $get_number($prefix, $next, $digit);
if ($number_exists($transaction_number)) {
do {
$next++;
$transaction_number = $get_number($prefix, $next, $digit);
} while ($number_exists($transaction_number));
setting(['transaction' . $suffix . '.number_next' => $next]);
setting()->save();
return app(TransactionNumber::class)->getNextNumber($type, $suffix, null);
}
return $transaction_number;
}
public function increaseNextTransactionNumber($suffix = ''): void
public function increaseNextTransactionNumber($type = 'income', $suffix = ''): void
{
$next = setting('transaction' . $suffix . '.number_next', 1) + 1;
setting(['transaction' . $suffix . '.number_next' => $next]);
setting()->save();
app(TransactionNumber::class)->increaseNextNumber($type, $suffix, null);
}
}

View File

@ -118,7 +118,7 @@ trait Uploads
return $path;
}
public function streamMedia($media)
public function streamMedia($media, $disposition = 'attachment')
{
return response()->streamDownload(
function() use ($media) {
@ -133,6 +133,7 @@ trait Uploads
'Content-Type' => $media->mime_type,
'Content-Length' => $media->size,
],
$disposition,
);
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Utilities;
use App\Interfaces\Utility\DocumentNumber as DocumentNumberInterface;
use App\Models\Common\Contact;
class DocumentNumber implements DocumentNumberInterface
{
public function getNextNumber(string $type, ?Contact $contact): string
{
$type = $this->resolveTypeAlias($type);
$prefix = setting($type . '.number_prefix');
$next = (string) setting($type . '.number_next');
$digit = (int) setting($type . '.number_digit');
return $prefix . str_pad($next, $digit, '0', STR_PAD_LEFT);
}
public function increaseNextNumber(string $type, ?Contact $contact): void
{
$type = $this->resolveTypeAlias($type);
$next = setting($type . '.number_next', 1) + 1;
setting([$type . '.number_next' => $next]);
setting()->save();
}
protected function resolveTypeAlias(string $type): string
{
if ($alias = config('type.document.' . $type . '.alias')) {
return $alias . '.' . str_replace('-', '_', $type);
}
return $type;
}
}

View File

@ -6,6 +6,7 @@ use App\Models\Auth\User;
use App\Models\Common\Company;
use App\Models\Common\Contact;
use App\Models\Document\Document;
use App\Traits\Cloud;
use Composer\InstalledVersions;
use Illuminate\Support\Facades\DB;
@ -13,7 +14,15 @@ class Info
{
public static function all()
{
return array_merge(static::versions(), [
static $info = [];
$is_cloud = (new class { use Cloud; })->isCloud();
if (! empty($info) || $is_cloud) {
return $info;
}
$info = array_merge(static::versions(), [
'api_key' => setting('apps.api_key'),
'ip' => static::ip(),
'companies' => Company::count(),
@ -22,11 +31,19 @@ class Info
'customers' => Contact::customer()->count(),
'php_extensions' => static::phpExtensions(),
]);
return $info;
}
public static function versions()
{
return [
static $versions = [];
if (! empty($versions)) {
return $versions;
}
$versions = [
'akaunting' => version('short'),
'laravel' => InstalledVersions::getPrettyVersion('laravel/framework'),
'php' => static::phpVersion(),
@ -35,6 +52,8 @@ class Info
'livewire' => InstalledVersions::getPrettyVersion('livewire/livewire'),
'omnipay' => InstalledVersions::getPrettyVersion('league/omnipay'),
];
return $versions;
}
public static function phpVersion()

View File

@ -3,8 +3,8 @@
namespace App\Utilities;
use App\Events\Module\PaymentMethodShowing;
use Cache;
use Date;
use App\Utilities\Date;
use Illuminate\Support\Facades\Cache;
class Modules
{

View File

@ -0,0 +1,44 @@
<?php
namespace App\Utilities;
use App\Interfaces\Utility\TransactionNumber as TransactionNumberInterface;
use App\Models\Banking\Transaction;
use App\Models\Common\Contact;
class TransactionNumber implements TransactionNumberInterface
{
public function getNextNumber($type, $suffix = '', ?Contact $contact): string
{
$prefix = setting('transaction' . $suffix . '.number_prefix');
$next = (string) setting('transaction' . $suffix . '.number_next');
$digit = (int) setting('transaction' . $suffix . '.number_digit');
$get_number = fn($prefix, $next, $digit) => $prefix . str_pad($next, $digit, '0', STR_PAD_LEFT);
$number_exists = fn($number) => Transaction::where('number', $number)->exists();
$transaction_number = $get_number($prefix, $next, $digit);
if ($number_exists($transaction_number)) {
do {
$next++;
$transaction_number = $get_number($prefix, $next, $digit);
} while ($number_exists($transaction_number));
setting(['transaction' . $suffix . '.number_next' => $next]);
setting()->save();
}
return $transaction_number;
}
public function increaseNextNumber($type, $suffix = '', ?Contact $contact): void
{
$next = setting('transaction' . $suffix . '.number_next', 1) + 1;
setting(['transaction' . $suffix . '.number_next' => $next]);
setting()->save();
}
}

View File

@ -3,10 +3,10 @@
namespace App\Utilities;
use App\Traits\SiteApi;
use Cache;
use Date;
use App\Utilities\Date;
use GrahamCampbell\Markdown\Facades\Markdown;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
class Versions
{
@ -56,7 +56,7 @@ class Versions
$versions = static::all($alias);
if (empty($versions[$alias])) {
return false;
return static::getVersionByAlias($alias);
}
return $versions[$alias];
@ -128,6 +128,30 @@ class Versions
return $versions;
}
public static function getVersionByAlias($alias)
{
$info = Info::all();
// Check core first
$url = 'core/version/' . $info['akaunting'] . '/' . $info['php'] . '/' . $info['mysql'] . '/' . $info['companies'];
$version = $info['akaunting'];
if ($alias != 'core') {
$version = module($alias)->get('version');
$url = 'apps/' . $alias . '/version/' . $version . '/' . $info['akaunting'];
}
// Get data from cache
$versions = Cache::get('versions', []);
$versions[$alias] = static::getLatestVersion($url, $version);
Cache::put('versions', $versions, Date::now()->addHour(6));
return $versions[$alias];
}
public static function getLatestVersion($url, $latest)
{
$version = new \stdClass();

View File

@ -3,6 +3,7 @@
use App\Models\Common\Company;
use App\Traits\DateTime;
use App\Traits\Sources;
use App\Traits\Modules;
use App\Utilities\Date;
use App\Utilities\Widgets;
@ -88,6 +89,20 @@ if (! function_exists('company')) {
}
}
if (! function_exists('module_is_enabled')) {
/**
* Check if a module is enabled.
*/
function module_is_enabled(string $alias): bool
{
$module = new class() {
use Modules;
};
return $module->moduleIsEnabled($alias);
}
}
if (! function_exists('company_id')) {
/**
* Get id of current company.

View File

@ -3,7 +3,6 @@
namespace App\View\Components\Documents\Form;
use App\Abstracts\View\Components\Documents\Form as Component;
use App\Models\Common\Company as Model;
class Company extends Component
{
@ -14,7 +13,7 @@ class Company extends Component
*/
public function render()
{
$company = Model::find(company_id());
$company = company();
$inputNameType = config('type.document.' . $this->type . '.route.parameter');

View File

@ -21,10 +21,6 @@ class NumberDigit extends Form
$this->name = 'number_digit';
}
if (empty($this->label)) {
$this->label = trans('settings.invoice.digit');
}
$this->number_digits = [
'1' => '1',
'2' => '2',

View File

@ -37,7 +37,7 @@ class Country extends Component
*/
public function render()
{
if (! empty($this->code) && in_array($this->code, trans('countries'))) {
if (! empty($this->code) && array_key_exists($this->code, trans('countries'))) {
$this->country = trans('countries.' . $this->code);
}

View File

@ -28,10 +28,10 @@ class Menu extends Component
public function getCompanies()
{
$companies = [];
if ($user = user()) {
$companies = $user->companies()->enabled()->limit(10)->get()->sortBy('name');
} else {
$companies = [];
}
return $companies;

View File

@ -50,7 +50,7 @@ class PaymentMethod extends Component
// check here protal or admin panel..
if (empty($type)) {
$type = Str::contains(request()->route()->getName(), 'portal') ? 'customer' : 'all';
$type = Str::contains(request()?->route()?->getName(), 'portal') ? 'customer' : 'all';
}
$payment_methods = Modules::getPaymentMethods($type);

View File

@ -75,6 +75,7 @@ class SearchString extends Component
'type' => $this->getFilterType($options),
'url' => $this->getFilterUrl($column, $options),
'values' => $this->getFilterValues($column, $options),
'value_option_fields' => $options['fields'] ?? [],
];
}
}

View File

@ -15,7 +15,7 @@ class ExpensesByCategory extends Widget
public function show()
{
Category::with('expense_transactions')->expense()->each(function ($category) {
Category::with('expense_transactions')->expense()->withSubCategory()->getWithoutChildren()->each(function ($category) {
$amount = 0;
$this->applyFilters($category->expense_transactions)->each(function ($transaction) use (&$amount) {

1144
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -188,6 +188,7 @@ return [
*/
App\Providers\App::class,
App\Providers\Auth::class,
App\Providers\Binding::class,
App\Providers\Blade::class,
// App\Providers\Broadcast::class,
App\Providers\Event::class,

View File

@ -372,7 +372,11 @@ return [
'contact_phone' => ['searchable' => true],
'contact_address' => ['searchable' => true],
'category_id' => [
'route' => ['categories.index', 'search=type:income enabled:1']
'route' => ['categories.index', 'search=type:income enabled:1'],
'fields' => [
'key' => 'id',
'value' => 'name',
],
],
'parent_id',
'recurring' => [

View File

@ -2,6 +2,7 @@
return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')),
// capture release as git sha
@ -81,4 +82,6 @@ return [
'traces_sampler' => [env('SENTRY_TRACES_SAMPLER_CLASS', 'App\\Exceptions\\Trackers\\Sentry'), 'tracesSampler'],
'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_PROFILES_SAMPLE_RATE'),
];

View File

@ -10,13 +10,13 @@ return [
'minor' => '0',
'patch' => '14',
'patch' => '15',
'build' => '',
'status' => 'Stable',
'date' => '25-April-2023',
'date' => '31-May-2023',
'time' => '17:00',

View File

@ -9,6 +9,7 @@ use App\Events\Document\DocumentReceived;
use App\Events\Document\DocumentSent;
use App\Events\Document\DocumentViewed;
use App\Events\Document\PaymentReceived;
use App\Interfaces\Utility\DocumentNumber;
use App\Jobs\Document\UpdateDocument;
use App\Models\Common\Contact;
use App\Models\Common\Item;
@ -70,7 +71,7 @@ class Document extends AbstractFactory
return [
'type' => Model::INVOICE_TYPE,
'document_number' => $this->getDocumentNumber(Model::INVOICE_TYPE),
'document_number' => $this->getDocumentNumber(Model::INVOICE_TYPE, $contact),
'category_id' => $this->company->categories()->income()->get()->random(1)->pluck('id')->first(),
'contact_id' => $contact->id,
'contact_name' => $contact->name,
@ -101,7 +102,7 @@ class Document extends AbstractFactory
return [
'type' => Model::BILL_TYPE,
'document_number' => $this->getDocumentNumber(Model::BILL_TYPE),
'document_number' => $this->getDocumentNumber(Model::BILL_TYPE, $contact),
'category_id' => $this->company->categories()->expense()->get()->random(1)->pluck('id')->first(),
'contact_id' => $contact->id,
'contact_name' => $contact->name,
@ -207,9 +208,11 @@ class Document extends AbstractFactory
{
$type = $this->getRawAttribute('type') . '-recurring';
$contact = Contact::find($this->getRawAttribute('contact_id'));
return $this->state([
'type' => $type,
'document_number' => $this->getDocumentNumber($type),
'document_number' => $this->getDocumentNumber($type, $contact),
'recurring_started_at' => $this->getRawAttribute('issued_at'),
'recurring_frequency' => 'daily',
'recurring_interval' => '1',
@ -263,11 +266,13 @@ class Document extends AbstractFactory
* Get document number
*
*/
public function getDocumentNumber($type)
public function getDocumentNumber($type, Contact $contact)
{
$document_number = $this->getNextDocumentNumber($type);
$utility = app(DocumentNumber::class);
$this->increaseNextDocumentNumber($type);
$document_number = $utility->getNextNumber($type, $contact);
$utility->increaseNextNumber($type, $contact);
return $document_number;
}

View File

@ -3,6 +3,7 @@
namespace Database\Factories;
use App\Abstracts\Factory;
use App\Interfaces\Utility\TransactionNumber;
use App\Models\Banking\Transaction as Model;
use App\Models\Common\Contact;
use App\Traits\Transactions;
@ -41,7 +42,7 @@ class Transaction extends Factory
return [
'company_id' => $this->company->id,
'type' => $this->type,
'number' => $this->getNumber(),
'number' => $this->getNumber($this->type),
'account_id' => setting('default.account'),
'paid_at' => $this->faker->dateTimeBetween(now()->startOfYear(), now()->endOfYear())->format('Y-m-d H:i:s'),
'amount' => $this->faker->randomFloat(2, 1, 1000),
@ -73,6 +74,7 @@ class Transaction extends Factory
return [
'type' => 'income',
'number' => $this->getNumber('income', '', $contact),
'contact_id' => $contact->id,
'category_id' => $this->company->categories()->income()->get()->random(1)->pluck('id')->first(),
];
@ -97,6 +99,7 @@ class Transaction extends Factory
return [
'type' => 'expense',
'number' => $this->getNumber('expense', '', $contact),
'contact_id' => $contact->id,
'category_id' => $this->company->categories()->expense()->get()->random(1)->pluck('id')->first(),
];
@ -110,9 +113,11 @@ class Transaction extends Factory
*/
public function recurring()
{
$type = $this->getRawAttribute('type') . '-recurring';
return $this->state([
'type' => $this->getRawAttribute('type') . '-recurring',
'number' => $this->getNumber('-recurring'),
'type' => $type,
'number' => $this->getNumber($type, '-recurring'),
'recurring_started_at' => Date::now()->format('Y-m-d H:i:s'),
'recurring_frequency' => 'daily',
'recurring_custom_frequency' => 'daily',
@ -129,11 +134,13 @@ class Transaction extends Factory
* Get transaction number
*
*/
public function getNumber($suffix = '')
public function getNumber($type, $suffix = '', $contact = null)
{
$number = $this->getNextTransactionNumber($suffix);
$utility = app(TransactionNumber::class);
$this->increaseNextTransactionNumber($suffix);
$number = $utility->getNextNumber($type, $suffix, $contact);
$utility->increaseNextNumber($type, $suffix ,$contact);
return $number;
}

View File

@ -0,0 +1,53 @@
<?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()
{
// Documents
Schema::table('documents', function(Blueprint $table) {
$table->index('contact_id');
});
// User Companies
Schema::table('user_companies', function(Blueprint $table) {
$table->index('user_id');
$table->index('company_id');
});
// User Roles
Schema::table('user_roles', function(Blueprint $table) {
$table->index('user_id');
$table->index('role_id');
});
// Transactions
Schema::table('transactions', function(Blueprint $table) {
$table->index('number');
});
// Roles
Schema::table('roles', function(Blueprint $table) {
$table->index('name');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
};

3054
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -33,7 +33,7 @@
"glightbox": "^3.2.0",
"json-schema": ">=0.4.0",
"laravel-mix-tailwind": "^0.1.2",
"lodash": ">=4.17.21",
"lodash": "^4.17.21",
"nprogress": "^0.2.0",
"popper.js": "^1.16.1",
"swiper": "^9.2.0",
@ -47,7 +47,10 @@
"vue-router": "^3.6.5",
"vue2-editor": "^2.10.3",
"vue2-transitions": "^0.3.0",
"vuedraggable": "^2.24.3"
"vuedraggable": "^2.24.3",
"moment": ">=2.29.4",
"qs": "^6.11.1",
"jsonwebtoken": "^9.0.0"
},
"devDependencies": {
"@babel/core": "^7.21.4",

2431
public/css/app.css vendored

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,8 @@
font-family: element-icons;
src: url(fonts/element-icons.woff) format("woff"),url(fonts/element-icons.ttf) format("truetype");
font-weight: 400;
font-display:"auto";font-style: normal
font-display:"auto";
font-style: normal;
}
[class*=" el-icon-"],[class^=el-icon-] {

View File

@ -264,8 +264,8 @@ export default {
},
sendEmailShow: {
type: Number,
default: 1,
type: [String, Number, Array, Object, Boolean],
default: '1',
description: "Created recurring model send automatically option"
},
sendEmailText: {

View File

@ -15,16 +15,17 @@
<span v-if="filter.operator" class="flex items-center bg-purple-lighter text-black border-2 border-body border-l border-r border-t-0 border-b-0 mt-3 px-3 py-4 text-sm cursor-pointer el-tag el-tag--small el-tag-operator" style="margin-left:0; margin-right:0;">
<span v-if="filter.operator == '='" class="material-icons text-2xl">drag_handle</span>
<span v-else-if="filter.operator == '><'" class="material-icons text-2xl transform rotate-90">height</span>
<span v-else class="w-5">
<img :src="not_equal_image" class="w-5 h-5 object-cover block" />
</span>
<img v-else :src="not_equal_image" class="w-5 h-5 object-cover block" />
<i v-if="!filter.value" class="mt-1 ltr:-right-2 rtl:left-0 rtl:right-0 el-tag__close el-icon-close " style="font-size: 16px;" @click="onFilterDelete(index)"></i>
<i v-if="!filter.value" class="mt-1 ltr:-right-2 rtl:left-0 rtl:right-0 el-tag__close el-icon-close" style="font-size: 16px;" @click="onFilterDelete(index)"></i>
</span>
<span v-if="filter.value" class="flex items-center bg-purple-lighter text-black border-0 mt-3 px-3 py-4 text-sm cursor-pointer el-tag el-tag--small el-tag-value">
{{ filter.value }}
<i class="mt-1 ltr:-right-2 rtl:left-0 rtl:right-0 el-tag__close el-icon-close " style="font-size: 16px;" @click="onFilterDelete(index)"></i>
<i class="mt-1 ltr:-right-2 rtl:left-0 rtl:right-0 el-tag__close el-icon-close" style="font-size: 16px;" @click="onFilterDelete(index)"></i>
</span>
</div>
</div>
@ -148,47 +149,57 @@ export default {
default: 'Search or filter results...',
description: 'Input placeholder'
},
selectPlaceholder: {
type: String,
},
enterPlaceholder: {
type: String,
},
searchText: {
type: String,
default: 'Search for this text',
description: 'Input placeholder'
},
operatorIsText: {
type: String,
default: 'is',
description: 'Operator is "="'
},
operatorIsNotText: {
type: String,
default: 'is not',
description: 'Operator is not "!="'
},
noDataText: {
type: String,
default: 'No Data',
description: "Selectbox empty options message"
},
noMatchingDataText: {
type: String,
default: 'No Matchign Data',
description: "Selectbox search option not found item message"
},
value: {
type: String,
default: null,
description: 'Search attribute value'
},
filters: {
type: Array,
default: () => [],
description: 'List of filters'
},
defaultFiltered: {
type: Array,
default: () => [],
@ -196,7 +207,6 @@ export default {
},
dateConfig: null
},
model: {
@ -423,10 +433,12 @@ export default {
let option = false;
let option_url = false;
let option_fields = {};
for (let i = 0; i < this.filter_list.length; i++) {
if (this.filter_list[i].key == value) {
option = this.filter_list[i].value;
option_fields = (this.filter_list[i]['value_option_fields']) ? this.filter_list[i].value_option_fields : {};
if (this.filter_list[i].values !== 'undefined' && Object.keys(this.filter_list[i].values).length) {
this.option_values[value] = this.convertOption(this.filter_list[i].values);
@ -475,7 +487,7 @@ export default {
}
}
if (!this.option_values[value] && option_url) {
if (! this.option_values[value] && option_url) {
if (option_url.indexOf('limit') === -1) {
option_url += ' limit:10';
}
@ -487,11 +499,19 @@ export default {
this.values = [];
data.forEach(function (item) {
if (Object.keys(option_fields).length) {
this.values.push({
key: (option_fields['key']) ? item[option_fields['key']] : (item.code) ? item.code : item.id,
value: (option_fields['value']) ? item[option_fields['value']] : (item.title) ? item.title : (item.display_name) ? item.display_name : item.name,
level: (option_fields['level']) ? item[option_fields['level']] : (item.level) ? item.level : null,
});
} else {
this.values.push({
key: (item.code) ? item.code : item.id,
value: (item.title) ? item.title : (item.display_name) ? item.display_name : item.name,
level: (item.level) ? item.level : null,
});
}
}, this);
this.option_values[value] = this.values;
@ -647,7 +667,7 @@ export default {
let values = [];
// Option set sort_option data
if (!Array.isArray(options)) {
if (! Array.isArray(options)) {
for (const [key, value] of Object.entries(options)) {
values.push({
key: (key).toString(),
@ -850,6 +870,7 @@ export default {
this.values.sort(function (a, b) {
var nameA = a.value.toUpperCase(); // ignore upper and lowercase
var nameB = b.value.toUpperCase(); // ignore upper and lowercase
if (nameA < nameB) {
return -1;
}
@ -857,6 +878,7 @@ export default {
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
});

View File

@ -1,5 +1,5 @@
<template>
<div class="card-item relative w-2/4 lg:w-3/4 h-48 m-auto" :class="{ '-active' : isCardFlipped }">
<div class="card-item relative w-2/4 lg:w-3/4 h-48 m-auto my-5" :class="{ '-active' : isCardFlipped }">
<div class="card-item__side h-full rounded-lg shadow-lg overflow-hidden" style="transform: perspective(2000px) rotateY(0deg) rotateX(0deg) rotate(0deg);
transform-style: preserve-3d;
transition: all 0.8s cubic-bezier(0.71, 0.03, 0.56, 0.85);

View File

@ -2,9 +2,24 @@
<div
@click="tryClose"
data-notify="container"
class="alert alert-notify fixed w-full sm:w-500 flex items-center justify-between ltr:right-0 rtl:left-0 sm:ltr:right-4 sm:rtl:left-4 p-4 text-black font-bold rounded-lg z-30"
:class="[
{ 'alert-with-icon': icon },
'alert alert-notify',
'fixed w-full sm:w-500 flex items-center justify-between',
{
'rtl:right-0 ltr:left-0' : horizontalAlign == 'left',
'sm:rtl:right-4 sm:ltr:left-4' : horizontalAlign == 'left',
},
{
'ltr:right-0 rtl:left-0' : horizontalAlign == 'right',
'sm:ltr:right-4 sm:rtl:left-4' : horizontalAlign == 'right',
},
'p-4',
'text-black font-bold',
'rounded-lg',
'z-30',
{
'alert-with-icon': icon
},
verticalAlign,
horizontalAlign,
alertType
@ -26,11 +41,10 @@
<span v-if="title" class="title">
<b>{{ title }}<br/></b>
</span>
<span v-if="message" v-html="message"></span>
<content-render
v-if="!message && component"
:component="component"
></content-render>
<content-render v-if="!message && component" :component="component"></content-render>
</span>
</div>
@ -39,49 +53,60 @@
class="close text-2xl"
data-dismiss="alert"
aria-label="Close"
@click="close">
@click="close"
>
<span aria-hidden="true">×</span>
</button>
</slot>
</div>
</template>
<script>
export default {
name: 'notification',
components: {
contentRender: {
props: ['component'],
render: h => h(this.component)
}
},
props: {
message: String,
title: {
type: String,
description: 'Notification title'
},
icon: {
type: String,
description: 'Notification icon'
},
verticalAlign: {
type: String,
default: 'top',
validator: value => {
let acceptedValues = ['top', 'bottom'];
return acceptedValues.indexOf(value) !== -1;
},
description: 'Vertical alignment of notification (top|bottom)'
},
horizontalAlign: {
type: String,
default: 'right',
validator: value => {
let acceptedValues = ['left', 'center', 'right'];
return acceptedValues.indexOf(value) !== -1;
},
description: 'Horizontal alignment of notification (left|center|right)'
},
type: {
type: String,
default: 'info',
@ -94,10 +119,12 @@
'warning',
'success'
];
return acceptedValues.indexOf(value) !== -1;
},
description: 'Notification type of notification (gray-300|blue-300|gray-300|red-300|orange-300|green-300)'
},
timeout: {
type: Number,
default: 5000,
@ -106,33 +133,40 @@
},
description: 'Notification timeout (closes after X milliseconds). Default is 5000 (5s)'
},
timestamp: {
type: Date,
default: () => new Date(),
description: 'Notification timestamp (used internally to handle notification removal correctly)'
},
component: {
type: [Object, Function],
description: 'Custom content component. Cane be a `.vue` component or render function'
},
showClose: {
type: Boolean,
default: true,
description: 'Whether to show close button'
},
closeOnClick: {
type: Boolean,
default: true,
description: 'Whether to close notification when clicking it\' body'
},
clickHandler: {
type: Function,
description: 'Custom notification click handler'
}
},
data() {
return {
elmHeight: 0,
typeByClass: {
'default': 'black-100',
'info': 'blue-100',
@ -141,6 +175,7 @@
'warning': 'orange-100',
'success': 'green-100',
},
textByClass: {
'default': 'black-600',
'info': 'blue-600',
@ -151,16 +186,20 @@
}
};
},
computed: {
hasIcon() {
return this.icon && this.icon.length > 0;
},
alertType() {
return `bg-${this.typeByClass[this.type]} text-${this.textByClass[this.type]}`;
},
customPosition() {
let initialMargin = 20;
let alertHeight = this.elmHeight + 10;
let sameAlertsCount = this.$notifications.state.filter(alert => {
return (
alert.horizontalAlign === this.horizontalAlign &&
@ -168,37 +207,51 @@
alert.timestamp <= this.timestamp
);
}).length;
if (this.$notifications.settings.overlap) {
sameAlertsCount = 1;
}
let pixels = (sameAlertsCount - 1) * alertHeight + initialMargin;
if (sameAlertsCount > 1) {
pixels = 30 + this.$parent.children[sameAlertsCount - 2].elm.offsetHeight;
}
let styles = {};
if (this.verticalAlign === 'top') {
styles.top = `${pixels}px`;
} else {
styles.bottom = `${pixels}px`;
}
return styles;
}
},
methods: {
close() {
this.$emit('close', this.timestamp);
},
tryClose(evt) {
if (this.clickHandler) {
this.clickHandler(evt, this);
}
if (this.closeOnClick) {
this.close();
}
}
},
mounted() {
this.elmHeight = this.$el.clientHeight;
if (this.timeout) {
setTimeout(this.close, this.timeout);
}
}
},
};
</script>

View File

@ -2,7 +2,8 @@
<div class="notifications">
<slide-y-up-transition :duration="transitionDuration"
group
mode="out-in">
mode="out-in"
>
<notification
v-for="notification in notifications"
v-bind="notification"
@ -14,6 +15,7 @@
</slide-y-up-transition>
</div>
</template>
<script>
import Notification from './Notification.vue';
import { SlideYUpTransition } from 'vue2-transitions';
@ -23,29 +25,35 @@
SlideYUpTransition,
Notification
},
props: {
transitionDuration: {
type: Number,
default: 200
},
overlap: {
type: Boolean,
default: false
}
},
data() {
return {
notifications: this.$notifications.state
};
},
methods: {
removeNotification(timestamp) {
this.$notifications.removeNotification(timestamp);
}
},
created() {
this.$notifications.settings.overlap = this.overlap;
},
watch: {
overlap: function (newVal) {
this.$notifications.settings.overlap = newVal;

View File

@ -2,6 +2,7 @@ import Notifications from './Notifications.vue';
const NotificationStore = {
state: [], // here the notifications will be added
settings: {
overlap: false,
verticalAlign: 'top',
@ -11,26 +12,37 @@ const NotificationStore = {
closeOnClick: true,
showClose: true
},
setOptions(options) {
this.settings = Object.assign(this.settings, options);
},
removeNotification(timestamp) {
const indexToDelete = this.state.findIndex(n => n.timestamp === timestamp);
if (indexToDelete !== -1) {
this.state.splice(indexToDelete, 1);
}
},
addNotification(notification) {
if (typeof notification === 'string' || notification instanceof String) {
notification = { message: notification };
notification = {
message: notification
};
}
notification.timestamp = new Date();
notification.timestamp.setMilliseconds(
notification.timestamp.getMilliseconds() + this.state.length
);
notification = Object.assign({}, this.settings, notification);
this.state.push(notification);
},
notify(notification) {
if (Array.isArray(notification)) {
notification.forEach(notificationInstance => {
@ -48,15 +60,18 @@ const NotificationsPlugin = {
data: {
notificationStore: NotificationStore
},
methods: {
notify(notification) {
this.notificationStore.notify(notification);
}
}
});
Vue.prototype.$notify = app.notify;
Vue.prototype.$notifications = app.notificationStore;
Vue.component('Notifications', Notifications);
if (options) {
NotificationStore.setOptions(options);
}

View File

@ -271,6 +271,8 @@ export default {
}
this.$notify({
verticalAlign: 'bottom',
horizontalAlign: 'left',
message: notify.message,
timeout: timeout,
icon: 'error_outline',
@ -467,6 +469,9 @@ export default {
onChangePaginationLimit(event) {
let path = '';
let split_href = window.location.href.split('#');
let href = split_href[0];
if (window.location.search.length) {
if (window.location.search.includes('limit')) {
let queries = [];
@ -494,10 +499,14 @@ export default {
});
} else {
path = window.location.href + '&limit=' + event.target.getAttribute("value");
path = href + '&limit=' + event.target.getAttribute("value");
}
} else {
path = window.location.href + '?limit=' + event.target.getAttribute("value");
path = href + '?limit=' + event.target.getAttribute("value");
}
if (split_href[1]) {
path += '#' + split_href[1];
}
window.location.href = path;
@ -505,6 +514,10 @@ export default {
// Dynamic component get path view and show it.
onDynamicComponent(path) {
if (! path) {
return;
}
axios.get(path)
.then(response => {
let html = response.data.html;
@ -562,6 +575,10 @@ export default {
},
onDynamicFormParams(path, params) {
if (! path) {
return;
}
let data = {};
for (const [key, value] of Object.entries(params)) {
@ -1001,7 +1018,7 @@ export default {
this.component = Vue.component('add-new-component', (resolve, reject) => {
resolve({
template: '<div id="dynamic-email-component"><akaunting-modal-add-new modal-dialog-class="max-w-screen-md" :show="email.modal" @submit="onSubmit" @cancel="onCancel" :buttons="email.buttons" :title="email.title" :is_component=true :message="email.html"></akaunting-modal-add-new></div>',
template: '<div id="dynamic-email-component"><akaunting-modal-add-new modal-dialog-class="max-w-screen-md" modal-position-top :show="email.modal" @submit="onSubmit" @cancel="onCancel" :buttons="email.buttons" :title="email.title" :is_component=true :message="email.html"></akaunting-modal-add-new></div>',
components: {
AkauntingDropzoneFileUpload,
@ -1112,6 +1129,8 @@ export default {
document.execCommand('copy');
this.$notify({
verticalAlign: 'bottom',
horizontalAlign: 'left',
message: this.share.success_message,
timeout: 5000,
icon: 'error_outline',

View File

@ -72,6 +72,8 @@ export default {
}
this.$notify({
verticalAlign: 'bottom',
horizontalAlign: 'left',
message: response.data.message,
timeout: timeout,
icon: "error_outline",
@ -92,6 +94,8 @@ export default {
}
this.$notify({
verticalAlign: 'bottom',
horizontalAlign: 'left',
message: event.message,
timeout: timeout,
icon: "error_outline",

View File

@ -54,6 +54,8 @@ const login = new Vue({
let type = notify.level;
this.$notify({
verticalAlign: 'bottom',
horizontalAlign: 'left',
message: notify.message,
timeout: 5000,
icon: '',

View File

@ -31,29 +31,4 @@ const app = new Vue({
bulk_action: new BulkAction('accounts'),
}
},
methods: {
onType(event) {
return;
let type = event.target.value;
switch(type) {
case 'credit_card':
this.onCreditCard();
break;
case 'bank':
default:
this.onBank();
break;
}
},
onCreditCard() {
},
onBank() {
},
}
});

View File

@ -69,6 +69,8 @@ const app = new Vue({
if (response.data.error) {
this.$notify({
verticalAlign: 'bottom',
horizontalAlign: 'left',
message: response.data.message,
timeout: 0,
icon: 'fas fa-bell',

View File

@ -621,6 +621,7 @@ const app = new Vue({
onChangeCurrency(currency_code) {
if (this.edit.status && this.edit.currency <= 2) {
this.edit.currency++;
return;
}

View File

@ -106,6 +106,8 @@ const app = new Vue({
add_to_cart_promise.then(response => {
if (response.data.success) {
this.$notify({
verticalAlign: 'bottom',
horizontalAlign: 'left',
message: response.data.message,
timeout: 0,
icon: "shopping_cart_checkout",

View File

@ -141,7 +141,7 @@ const app = new Vue({
});
},
// Change currency get money
// Change currency get money override because remove form currency_code and currency_rate column
onChangeCurrency(currency_code) {
if (! currency_code) {
return;

View File

@ -1,5 +1,5 @@
<template>
<div class="relative bg-body z-10 rounded-lg shadow-2xl p-5 ltr:pr-0 rtl:pl-0 sm:py-10 sm:ltr:pl-10 sm:rtl:pr-10 overflow-hidden">
<div class="relative bg-body z-10 rounded-lg shadow-2xl p-5 sm:p-10 full-height-mobile overflow-hidden">
<WizardSteps :active_state="active"></WizardSteps>
<div class="flex flex-col justify-between -mt-5 sm:mt-0" style="height:565px;">
@ -42,7 +42,7 @@
</div>
</div>
<div class="relative w-1/2 right-0 ltr:pl-10 rtl:pr-10 mt-3 hidden lg:flex lg:flex-col">
<div class="absolute w-1/2 right-0 ltr:pl-10 rtl:pr-10 mt-3 hidden lg:flex lg:flex-col">
<div class="flex flex-col ltr:items-start rtl:items-end bg-purple ltr:rounded-tl-lg ltr:rounded-bl-lg rtl:rounded-tr-lg rtl:rounded-br-lg p-6">
<div class="w-48 text-white text-left text-2xl font-semibold leading-9">
{{ translations.finish.apps_managing }}
@ -50,7 +50,7 @@
<div style="width:372px; height:372px;"></div>
<img :src="image_src" class="absolute top-3 right-2" alt="" />
<img :src="image_src" class="absolute top-3 right-2" alt="Akaunting" />
</div>
<base-button
@ -112,6 +112,8 @@ export default {
})
.catch((error) => {
this.$notify({
verticalAlign: 'bottom',
horizontalAlign: 'left',
message: this.translations.finish.error_message,
timeout: 1000,
icon: "",

View File

@ -72,6 +72,7 @@ return [
'attachments' => 'Adjunt|Adjunts',
'histories' => 'Història|Històries',
'your_notifications' => 'La teva notificació|Les teves notificacions',
'employees' => 'Empleat|Empleats',
'welcome' => 'Benvingut/da',
'banking' => 'Bancs',
@ -228,6 +229,7 @@ return [
'preview_mode' => 'Mode de previsualització',
'go_back' => 'Torna a :type',
'validation_error' => 'Error de validació',
'dismiss' => 'Ignora',
'card' => [
'cards' => 'Tarjeta|Tarjetes',

Some files were not shown because too many files have changed in this diff Show More