49 Commits
1.0.0 ... 1.x

Author SHA1 Message Date
Darko Gjorgjijoski
27a511bc09 Update version
[ci-skip]
2024-07-13 03:11:14 +02:00
Darko Gjorgjijoski
1317d57ef8 Update readme.md 2024-07-13 03:07:18 +02:00
Darko Gjorgjijoski
ba85f407d6 Revert "Add version.md file"
This reverts commit 75b0c264dc.
2024-07-13 02:49:02 +02:00
Darko Gjorgjijoski
b37019f845 Set the default database to SQLite 2024-07-13 02:29:20 +02:00
Darko Gjorgjijoski
75b0c264dc Add version.md file 2024-07-13 02:14:41 +02:00
Darko Gjorgjijoski
98852686c7 Add doctrine/dbal and run pint 2024-07-13 02:03:22 +02:00
Jay Muntz
72311db1bd Fix SES configuration process to set correct SES_KEY and SES_SECRET (#74)
* Fix SES configuration process to set correct SES_KEY and SES_SECRET

* Fixed SES from name

---------

Co-authored-by: Jay Muntz <jaymuntz@jaymuntz.com>
2024-05-31 12:46:06 +02:00
Darko Gjorgjijoski
1e530e0387 Include lang directory in the dist build (#65)
* Include lang/ dir in the build

* Bump version
2024-05-04 15:27:40 +02:00
Darko Gjorgjijoski
fa777d673f Update release migration rollback to point to 1.2.0 2024-05-04 13:48:42 +02:00
Darko Gjorgjijoski
8570252db5 Add missing migration 2024-05-04 13:35:51 +02:00
Darko Gjorgjijoski
ecb029758b Add Libyan Dinar (LD) (#63)
* Add Libyan Dinar (LD)

* Fix code style
2024-05-04 12:06:07 +02:00
Timo
6ea112c04f fix EnvironmentManager updateEnv (#60)
* fix EnvironmentManager updateEnv

* fix EnvironmentManager encode method

* fix code style
2024-05-04 10:35:35 +02:00
Timo
2784318ebf Migrate polymorphic relationships (#58)
* add crater media model type migration

* migrate all morph type fields

* fix code style

* migrate custom_field_values
2024-04-21 00:03:34 +02:00
Darko Gjorgjijoski
bb580f4b88 Restore open-direction top in Account language picker
Follow up to #55
2024-04-20 23:26:10 +02:00
Timo
8c83df558c Add Company VAT-ID and Tax-ID (#54)
* add company vat_id & tax_id field

* add tax & vat id field in company settings

* fix vat & tax id validation

* add german vat & tax id translation

* add translations for pdf

* add vat_id and tax_id field before timestamps

* make fields nullable and fix code style
2024-04-20 23:08:32 +02:00
Timo
dc8a85538f Support S3 compatible storage services (#56)
* add s3compat filesystem driver

* add s3compat ui modal

* fix code style
2024-04-16 17:24:56 +02:00
Darko Gjorgjijoski
093b2acc24 Merge branch 'master' of github.com:InvoiceShelf/InvoiceShelf 2024-04-16 03:02:27 +02:00
Timo
df85fd6a0a Fix base multi select open direction top (#55) 2024-04-16 03:01:53 +02:00
Darko Gjorgjijoski
db4396f160 Remove duplicate noLabel in dialog store 2024-04-16 03:01:14 +02:00
Darko Gjorgjijoski
1831560a62 Fix language switching. It now switches instantly 2024-04-16 02:58:44 +02:00
mchev
223678e5bd Fix locales issue #43 (#46)
* Fix locales issue #43

* Adding open-direction bottom to the language multiselect
2024-03-27 11:00:36 +01:00
Darko Gjorgjijoski
9bb4963e8a Fixes/backup issues (#51)
* Fix: Error related to undefined Backup::size()

* Fix: Disable signals if PCNTL isn't loaded to avoid fatal error (Fixes SIGINT is not defined on environments that are missing the PCNTL library)
2024-03-27 01:15:49 +01:00
Darko Gjorgjijoski
8788f3d504 Tax calculation issue (#38)
* fix initial tax per item issue

* remove commit in estimate storage

* add changes in tax per item calculation

* add validation on requests

* fix minimum total issue

* fix table pagination filter issue

* minor fix

* remove compound interest and remove unused code

---------

Co-authored-by: yashkanakiya <yashkanakiya281297@gmail.com>
Co-authored-by: dhruvbhattt <dhruvbhatt7790@gmail.com>
Co-authored-by: gdarko <dg@darkog.com>
2024-02-18 10:54:12 +01:00
Darko Gjorgjijoski
0d006846d5 Merge pull request #37 from InvoiceShelf/bouncer-hotfix
Bouncer hotfix
2024-02-18 01:25:47 +01:00
gdarko
d2559c8773 Fix formatting 2024-02-18 00:49:15 +01:00
Darko Gjorgjijoski
20a489e52d Merge pull request #35 from dariuszz123/bugfix/missing-access-token-table-column
Added expires_at column to personal_access_tokens table
2024-02-18 00:27:13 +01:00
gdarko
e73ab3d4ee Override the default bouncer scope class with a temporary hotfix to the roles issue 2024-02-18 00:13:42 +01:00
gdarko
1c7274287d Lock silber/bouncer to 1.0.1 for now 2024-02-18 00:12:33 +01:00
Darius Kristapavicius
5ba8639ac8 Added expires_at column to personal_access_tokens table 2024-02-17 23:51:21 +02:00
gdarko
44153c2993 Improve 110 migration rollback 2024-02-11 20:43:03 +01:00
gdarko
b9d5b8247c Fix: Wrong migration name 2024-02-11 20:42:01 +01:00
Darko Gjorgjijoski
39879cb4b4 Merge pull request #29 from InvoiceShelf/database/old-leftovers
Release migration and improvements to the database
2024-02-11 20:25:49 +01:00
Darko Gjorgjijoski
7d7b48a5b7 Merge pull request #28 from InvoiceShelf/installer-improvements-phase1
Installer improvements
2024-02-11 20:25:32 +01:00
gdarko
e0176139c2 Remove unecessary db install flag delete call 2024-02-11 20:07:59 +01:00
gdarko
29ed7f437a Fix code style 2024-02-11 13:58:38 +01:00
gdarko
020a0741de Add error logging to install process :: db marker utilities 2024-02-11 13:55:32 +01:00
gdarko
d2ad2222be Fix code style 2024-02-11 09:41:23 +01:00
gdarko
2358969050 Add release migration and fix some old database leftovers 2024-02-11 09:38:21 +01:00
gdarko
c7c70dd3ef Encoide .env value in case of spaces or special characters 2024-02-11 02:41:14 +01:00
gdarko
b806efd66d Refactor EnvironmentManager to fix weird installation issues 2024-02-11 01:44:05 +01:00
gdarko
b85538dbc0 Fix code style 2024-02-10 01:36:53 +01:00
gdarko
8918ea5124 Fix image extension validation
Issue #21
2024-02-10 01:31:44 +01:00
Darko Gjorgjijoski
91d37673aa Merge pull request #25 from danybmx/18-allow-negative-taxes
feat: Allow negative taxes
2024-02-10 00:49:09 +01:00
Daniel Rodríguez
b819440e76 feat: allow negative taxes 2024-02-08 19:19:32 +01:00
Darko Gjorgjijoski
36424bbf05 Update code style 2024-02-08 03:37:17 +01:00
Darko Gjorgjijoski
e14a248f24 Fix: Status set incorrectly after updating invoice
Issue: https://github.com/crater-invoice/crater/issues/955, #23
2024-02-08 03:09:56 +01:00
Darko Gjorgjijoski
acf3ca2c26 Fix Discord link 2024-02-08 02:33:27 +01:00
Darko Gjorgjijoski
e7e9c57552 Improvements to the menu generation, add safety checks 2024-02-08 01:54:05 +01:00
Darko Gjorgjijoski
49a11f08e9 Improvements to the Install Utilities 2024-02-08 01:53:32 +01:00
120 changed files with 2474 additions and 1686 deletions

View File

@@ -4,12 +4,12 @@ APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://invoiceshelf.test
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=invoiceshelf
DB_USERNAME=invoiceshelf
DB_PASSWORD="invoiceshelf"
DB_CONNECTION=sqlite
DB_HOST=
DB_PORT=
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
@@ -28,6 +28,9 @@ MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=
MAIL_FROM_NAME=
MAIL_FROM_ADDRESS=
PUSHER_APP_ID=
PUSHER_KEY=
PUSHER_SECRET=

1
.gitignore vendored
View File

@@ -21,3 +21,4 @@ Homestead.yaml
.DS_Store
.php-cs-fixer.cache
/storage/fonts*
package-lock.json

View File

@@ -25,6 +25,7 @@ dist-gen: clean composer npm-build
@cp -r public/robots.txt InvoiceShelf/public
@cp -r public/web.config InvoiceShelf/public
@cp -r resources InvoiceShelf
@cp -r lang InvoiceShelf
@cp -r routes InvoiceShelf
@cp -r storage InvoiceShelf
@cp -r vendor InvoiceShelf 2> /dev/null || true

View File

@@ -0,0 +1,38 @@
<?php
namespace InvoiceShelf\Bouncer\Scopes;
use Silber\Bouncer\Database\Scope\Scope;
class DefaultScope extends Scope
{
public function applyToModelQuery($query, $table = null)
{
if (is_null($this->scope) || $this->onlyScopeRelations) {
return $query;
}
if (is_null($table)) {
$table = $query->getModel()->getTable();
}
return $this->applyToQuery($query, $table);
}
public function applyToRelationQuery($query, $table)
{
if (is_null($this->scope)) {
return $query;
}
return $this->applyToQuery($query, $table);
}
protected function applyToQuery($query, $table)
{
return $query->where(function ($query) use ($table) {
$query->where("{$table}.scope", $this->scope)
->orWhereNull("{$table}.scope");
});
}
}

View File

@@ -36,7 +36,7 @@ class BackupsController extends ApiController
return [
'path' => $backup->path(),
'created_at' => $backup->date()->format('Y-m-d H:i:s'),
'size' => Format::humanReadableSize($backup->size()),
'size' => Format::humanReadableSize($backup->sizeInBytes()),
];
})
->toArray();

View File

@@ -41,7 +41,7 @@ class DownloadBackupController extends ApiController
$downloadHeaders = [
'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0',
'Content-Type' => 'application/zip',
'Content-Length' => $backup->size(),
'Content-Length' => $backup->sizeInBytes(),
'Content-Disposition' => 'attachment; filename="'.$fileName.'"',
'Pragma' => 'public',
];

View File

@@ -94,6 +94,16 @@ class DiskController extends Controller
break;
case 's3compat':
$diskData = [
'endpoint' => '',
'key' => '',
'secret' => '',
'region' => '',
'bucket' => '',
'root' => '',
];
case 'doSpaces':
$diskData = [
'key' => '',
@@ -160,6 +170,10 @@ class DiskController extends Controller
'name' => 'Amazon S3',
'value' => 's3',
],
[
'name' => 'S3 Compatible Storage',
'value' => 's3compat',
],
[
'name' => 'Digital Ocean Spaces',
'value' => 'doSpaces',

View File

@@ -11,11 +11,13 @@ class FinishController extends Controller
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/
public function __invoke(Request $request)
{
InstallUtils::createDbMarker();
if (! InstallUtils::createDbMarker()) {
\Log::error('Install: Unable to create db marker.');
}
return response()->json(['success' => true]);
}

View File

@@ -17,10 +17,6 @@ class InstallationMiddleware
public function handle($request, Closure $next)
{
if (! InstallUtils::isDbCreated() || Setting::getSetting('profile_complete') !== 'COMPLETED') {
if (InstallUtils::dbMarkerExists()) {
InstallUtils::deleteDbMarker();
}
return redirect('/installation');
}

View File

@@ -69,6 +69,8 @@ class CompaniesRequest extends FormRequest
return collect($this->validated())
->only([
'name',
'vat_id',
'tax_id',
])
->merge([
'owner_id' => $this->user()->id,

View File

@@ -29,6 +29,12 @@ class CompanyRequest extends FormRequest
'required',
Rule::unique('companies')->ignore($this->header('company'), 'id'),
],
'vat_id' => [
'nullable',
],
'tax_id' => [
'nullable',
],
'slug' => [
'nullable',
],
@@ -44,6 +50,8 @@ class CompanyRequest extends FormRequest
->only([
'name',
'slug',
'vat_id',
'tax_id',
])
->toArray();
}

View File

@@ -45,15 +45,21 @@ class EstimatesRequest extends FormRequest
'nullable',
],
'discount' => [
'numeric',
'required',
],
'discount_val' => [
'integer',
'required',
],
'sub_total' => [
'integer',
'required',
],
'total' => [
'integer',
'numeric',
'max:99999999',
'required',
],
'tax' => [
@@ -77,9 +83,11 @@ class EstimatesRequest extends FormRequest
'required',
],
'items.*.quantity' => [
'integer',
'required',
],
'items.*.price' => [
'integer',
'required',
],
];

View File

@@ -45,15 +45,21 @@ class InvoicesRequest extends FormRequest
'nullable',
],
'discount' => [
'numeric',
'required',
],
'discount_val' => [
'integer',
'required',
],
'sub_total' => [
'integer',
'required',
],
'total' => [
'integer',
'numeric',
'max:99999999',
'required',
],
'tax' => [
@@ -77,9 +83,11 @@ class InvoicesRequest extends FormRequest
'required',
],
'items.*.quantity' => [
'integer',
'required',
],
'items.*.price' => [
'integer',
'required',
],
];

View File

@@ -43,15 +43,21 @@ class RecurringInvoiceRequest extends FormRequest
'nullable',
],
'discount' => [
'numeric',
'required',
],
'discount_val' => [
'integer',
'required',
],
'sub_total' => [
'integer',
'required',
],
'total' => [
'integer',
'numeric',
'max:99999999',
'required',
],
'tax' => [

View File

@@ -17,6 +17,8 @@ class CompanyResource extends JsonResource
return [
'id' => $this->id,
'name' => $this->name,
'vat_id' => $this->vat_id,
'tax_id' => $this->tax_id,
'logo' => $this->logo,
'logo_path' => $this->logo_path,
'unique_hash' => $this->unique_hash,

View File

@@ -44,6 +44,9 @@ class CreateBackupJob implements ShouldQueue
config(['backup.backup.destination.disks' => [$prefix.$fileDisk->driver]]);
$backupJob = BackupJobFactory::createFromArray(config('backup'));
if (! defined('SIGINT')) {
$backupJob->disableSignals();
}
if ($this->data['option'] === 'only-db') {
$backupJob->dontBackupFilesystem();

View File

@@ -401,7 +401,10 @@ class Invoice extends Model implements HasMedia
$data['base_due_amount'] = $data['due_amount'] * $data['exchange_rate'];
$data['customer_sequence_number'] = $serial->nextCustomerSequenceNumber;
$this->changeInvoiceStatus($data['due_amount']);
$statusData = $this->getInvoiceStatusByAmount($data['due_amount']);
if (! empty($statusData)) {
$data = array_merge($data, $statusData);
}
$this->update($data);
@@ -691,27 +694,52 @@ class Invoice extends Model implements HasMedia
$this->changeInvoiceStatus($this->due_amount);
}
public function changeInvoiceStatus($amount)
/**
* Set the invoice status from amount.
*
* @return array
*/
public function getInvoiceStatusByAmount($amount)
{
if ($amount < 0) {
return [
'error' => 'invalid_amount',
];
return [];
}
if ($amount == 0) {
$this->status = Invoice::STATUS_COMPLETED;
$this->paid_status = Invoice::STATUS_PAID;
$this->overdue = false;
$data = [
'status' => Invoice::STATUS_COMPLETED,
'paid_status' => Invoice::STATUS_PAID,
'overdue' => false,
];
} elseif ($amount == $this->total) {
$this->status = $this->getPreviousStatus();
$this->paid_status = Invoice::STATUS_UNPAID;
$data = [
'status' => $this->getPreviousStatus(),
'paid_status' => Invoice::STATUS_UNPAID,
];
} else {
$this->status = $this->getPreviousStatus();
$this->paid_status = Invoice::STATUS_PARTIALLY_PAID;
$data = [
'status' => $this->getPreviousStatus(),
'paid_status' => Invoice::STATUS_PARTIALLY_PAID,
];
}
$this->save();
return $data;
}
/**
* Changes the invoice status right away
*
* @return string[]|void
*/
public function changeInvoiceStatus($amount)
{
$status = $this->getInvoiceStatusByAmount($amount);
if (! empty($status)) {
foreach ($status as $key => $value) {
$this->setAttribute($key, $value);
}
$this->save();
}
}
public static function deleteInvoices($ids)

View File

@@ -4,7 +4,9 @@ namespace InvoiceShelf\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;
use InvoiceShelf\Bouncer\Scopes\DefaultScope;
use InvoiceShelf\Space\InstallUtils;
use Silber\Bouncer\Database\Models as BouncerModels;
class AppServiceProvider extends ServiceProvider
{
@@ -16,7 +18,6 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
Paginator::useBootstrapThree();
$this->loadJsonTranslationsFrom(resource_path('scripts/locales'));
if (InstallUtils::isDbCreated()) {
$this->addMenus();
@@ -30,7 +31,7 @@ class AppServiceProvider extends ServiceProvider
*/
public function register()
{
//
BouncerModels::scope(new DefaultScope);
}
public function addMenus()

View File

@@ -32,11 +32,18 @@ class Base64Mime implements Rule
$this->attribute = $attribute;
try {
$data = json_decode($value)->data;
$decoded = json_decode(trim($value));
$name = ! empty($decoded->name) ? $decoded->name : '';
$data = ! empty($decoded->data) ? $decoded->data : '';
} catch (\Exception $e) {
return false;
}
$extension = pathinfo($name, PATHINFO_EXTENSION);
if (! in_array($extension, $this->extensions)) {
return false;
}
$pattern = '/^data:\w+\/[\w\+]+;base64,[\w\+\=\/]+$/';
if (! preg_match($pattern, $data)) {

View File

@@ -3,7 +3,6 @@
namespace InvoiceShelf\Space;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use InvoiceShelf\Http\Requests\DatabaseEnvironmentRequest;
use InvoiceShelf\Http\Requests\DiskEnvironmentRequest;
@@ -17,6 +16,11 @@ class EnvironmentManager
*/
private $envPath;
/**
* @var string
*/
private $delimiter = "\n";
/**
* Set the .env and .env.example paths.
*/
@@ -25,6 +29,73 @@ class EnvironmentManager
$this->envPath = base_path('.env');
}
/**
* Returns the .env contents
*
* @return false|string
*/
private function getEnvContents()
{
return file_get_contents($this->envPath);
}
/**
* Updates .env file - inspired by Akaunting
*
* @return bool
*/
public function updateEnv(array $data)
{
if (empty($data) || ! is_array($data) || ! is_file($this->envPath)) {
return false;
}
$env = $this->getEnvContents();
$env = explode($this->delimiter, $env);
foreach ($data as $data_key => $data_value) {
$updated = false;
foreach ($env as $env_key => $env_value) {
$entry = explode('=', $env_value, 2);
// Check if new or old key
if ($entry[0] == $data_key) {
$env[$env_key] = $data_key.'='.$this->encode($data_value);
$updated = true;
}
}
// Lets create if not available
if (! $updated) {
$env[] = $data_key.'='.$this->encode($data_value);
}
}
$env = implode($this->delimiter, $env);
file_put_contents(base_path('.env'), $env);
return true;
}
/**
* Encodes value for .env
*
* @return mixed|string
*/
private function encode($str)
{
if ((strpos($str, ' ') !== false || preg_match('/'.preg_quote('^\'£$%^&*()}{@#~?><,@|-=-_+-¬', '/').'/', $str)) && ($str[0] != '"' || $str[strlen($str) - 1] != '"')) {
$str = '"'.$str.'"';
}
return $str;
}
/**
* Save the database content to the .env file.
*
@@ -32,59 +103,27 @@ class EnvironmentManager
*/
public function saveDatabaseVariables(DatabaseEnvironmentRequest $request)
{
$oldDatabaseData =
'DB_CONNECTION='.config('database.default')."\n";
$newDatabaseData =
'DB_CONNECTION='.$request->database_connection."\n";
$dbEnv = [
'APP_URL' => $request->get('app_url'),
'DB_CONNECTION' => $request->get('database_connection'),
'SANCTUM_STATEFUL_DOMAINS' => $request->get('app_domain'),
'SESSION_DOMAIN' => explode(':', $request->get('app_domain'))[0],
];
if ($request->has('database_username') && $request->has('database_password')) {
if (env('DB_USERNAME') && env('DB_HOST')) {
$oldDatabaseData = $oldDatabaseData.
'DB_HOST='.config('database.connections.'.config('database.default').'.host')."\n".
'DB_PORT='.config('database.connections.'.config('database.default').'.port')."\n".
'DB_DATABASE='.config('database.connections.'.config('database.default').'.database')."\n".
'DB_USERNAME='.config('database.connections.'.config('database.default').'.username')."\n".
'DB_PASSWORD="'.config('database.connections.'.config('database.default').'.password')."\"\n\n";
} else {
$oldDatabaseData = $oldDatabaseData.
'DB_DATABASE='.config('database.connections.'.config('database.default').'.database')."\n\n";
}
$dbEnv['DB_HOST'] = $request->get('database_hostname');
$dbEnv['DB_PORT'] = $request->get('database_port');
$dbEnv['DB_DATABASE'] = $request->get('database_name');
$dbEnv['DB_USERNAME'] = $request->get('database_username');
$dbEnv['DB_PASSWORD'] = $request->get('database_password');
$newDatabaseData = $newDatabaseData.
'DB_HOST='.$request->database_hostname."\n".
'DB_PORT='.$request->database_port."\n".
'DB_DATABASE='.$request->database_name."\n".
'DB_USERNAME='.$request->database_username."\n".
'DB_PASSWORD="'.$request->database_password."\"\n\n";
} else {
if (env('DB_USERNAME') && env('DB_HOST')) {
$oldDatabaseData = $oldDatabaseData.
'DB_HOST='.config('database.connections.'.config('database.default').'.host')."\n".
'DB_PORT='.config('database.connections.'.config('database.default').'.port')."\n".
'DB_DATABASE='.config('database.connections.'.config('database.default').'.database')."\n".
'DB_USERNAME='.config('database.connections.'.config('database.default').'.username')."\n".
'DB_PASSWORD="'.config('database.connections.'.config('database.default').'.password')."\"\n\n";
} else {
$oldDatabaseData = $oldDatabaseData.
'DB_DATABASE='.config('database.connections.'.config('database.default').'.database')."\n\n";
}
$newDatabaseData = $newDatabaseData.
'DB_DATABASE='.$request->database_name."\n\n";
$dbEnv['DB_DATABASE'] = $request->get('database_name');
}
try {
$conn = $this->checkDatabaseConnection($request);
// $requirement = $this->checkVersionRequirements($request, $conn);
// if ($requirement) {
// return [
// 'error' => 'minimum_version_requirement',
// 'requirement' => $requirement,
// ];
// }
$this->checkDatabaseConnection($request);
if (\Schema::hasTable('users')) {
return [
@@ -98,29 +137,7 @@ class EnvironmentManager
}
try {
file_put_contents($this->envPath, str_replace(
$oldDatabaseData,
$newDatabaseData,
file_get_contents($this->envPath)
));
file_put_contents($this->envPath, str_replace(
'APP_URL='.config('app.url'),
'APP_URL='.$request->app_url,
file_get_contents($this->envPath)
));
file_put_contents($this->envPath, str_replace(
'SANCTUM_STATEFUL_DOMAINS='.env('SANCTUM_STATEFUL_DOMAINS'),
'SANCTUM_STATEFUL_DOMAINS='.$request->app_domain,
file_get_contents($this->envPath)
));
file_put_contents($this->envPath, str_replace(
'SESSION_DOMAIN='.config('session.domain'),
'SESSION_DOMAIN='.explode(':', $request->app_domain)[0],
file_get_contents($this->envPath)
));
$this->updateEnv($dbEnv);
} catch (Exception $e) {
return [
'error' => 'database_variables_save_error',
@@ -133,26 +150,27 @@ class EnvironmentManager
}
/**
* @return bool
* Returns PDO object if all ok.
*
* @return \Closure|\PDO
*/
private function checkDatabaseConnection(DatabaseEnvironmentRequest $request)
{
$connection = $request->database_connection;
$connection = $request->get('database_connection');
$settings = config("database.connections.$connection");
$settings = config("database.connections.$connection");
$connectionArray = array_merge($settings, [
'driver' => $connection,
'database' => $request->database_name,
'database' => $request->get('database_name'),
]);
if ($request->has('database_username') && $request->has('database_password')) {
$connectionArray = array_merge($connectionArray, [
'username' => $request->database_username,
'password' => $request->database_password,
'host' => $request->database_hostname,
'port' => $request->database_port,
'username' => $request->get('database_username'),
'password' => $request->get('database_password'),
'host' => $request->get('database_hostname'),
'port' => $request->get('database_port'),
]);
}
@@ -167,86 +185,19 @@ class EnvironmentManager
return DB::connection()->getPdo();
}
/**
* @return bool
*/
private function checkVersionRequirements(DatabaseEnvironmentRequest $request, $conn)
{
$connection = $request->database_connection;
$checker = new RequirementsChecker();
$phpSupportInfo = $checker->checkPHPVersion(
config('invoiceshelf.min_php_version')
);
if (! $phpSupportInfo['supported']) {
return $phpSupportInfo;
}
$dbSupportInfo = [];
switch ($connection) {
case 'mysql':
$dbSupportInfo = $checker->checkMysqlVersion($conn);
break;
case 'pgsql':
$conn = pg_connect("host={$request->database_hostname} port={$request->database_port} dbname={$request->database_name} user={$request->database_username} password={$request->database_password}");
$dbSupportInfo = $checker->checkPgsqlVersion(
$conn,
config('invoiceshelf.min_pgsql_version')
);
break;
case 'sqlite':
$dbSupportInfo = $checker->checkSqliteVersion(
config('invoiceshelf.min_sqlite_version')
);
break;
}
if (! $dbSupportInfo['supported']) {
return $dbSupportInfo;
}
return false;
}
/**
* Save the mail content to the .env file.
*
* @param Request $request
* @return array
*/
public function saveMailVariables(MailEnvironmentRequest $request)
{
$mailData = $this->getMailData($request);
$mailEnv = $this->getMailConfiguration($request);
try {
file_put_contents($this->envPath, str_replace(
$mailData['old_mail_data'],
$mailData['new_mail_data'],
file_get_contents($this->envPath)
));
if ($mailData['extra_old_mail_data']) {
file_put_contents($this->envPath, str_replace(
$mailData['extra_old_mail_data'],
$mailData['extra_mail_data'],
file_get_contents($this->envPath)
));
} else {
file_put_contents(
$this->envPath,
"\n".$mailData['extra_mail_data'],
FILE_APPEND
);
}
$this->updateEnv($mailEnv);
} catch (Exception $e) {
return [
'error' => 'mail_variables_save_error',
@@ -258,193 +209,103 @@ class EnvironmentManager
];
}
private function getMailData($request)
/**
* Returns the mail configuration
*
* @param MailEnvironmentRequest $request
* @return array
*/
private function getMailConfiguration($request)
{
$mailFromCredential = '';
$extraMailData = '';
$extraOldMailData = '';
$oldMailData = '';
$newMailData = '';
$mailEnv = [];
if (env('MAIL_FROM_ADDRESS') !== null && env('MAIL_FROM_NAME') !== null) {
$mailFromCredential =
'MAIL_FROM_ADDRESS='.config('mail.from.address')."\n".
'MAIL_FROM_NAME="'.config('mail.from.name')."\"\n\n";
}
$driver = $request->get('mail_driver');
switch ($request->mail_driver) {
switch ($driver) {
case 'smtp':
$oldMailData =
'MAIL_DRIVER='.config('mail.driver')."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
$mailFromCredential;
$newMailData =
'MAIL_DRIVER='.$request->mail_driver."\n".
'MAIL_HOST='.$request->mail_host."\n".
'MAIL_PORT='.$request->mail_port."\n".
'MAIL_USERNAME='.$request->mail_username."\n".
'MAIL_PASSWORD='.$request->mail_password."\n".
'MAIL_ENCRYPTION='.$request->mail_encryption."\n\n".
'MAIL_FROM_ADDRESS='.$request->from_mail."\n".
'MAIL_FROM_NAME="'.$request->from_name."\"\n\n";
$mailEnv = [
'MAIL_DRIVER' => $request->get('mail_driver'),
'MAIL_HOST' => $request->get('mail_host'),
'MAIL_PORT' => $request->get('mail_port'),
'MAIL_USERNAME' => $request->get('mail_username'),
'MAIL_PASSWORD' => $request->get('mail_password'),
'MAIL_ENCRYPTION' => $request->get('mail_encryption'),
'MAIL_FROM_ADDRESS' => $request->get('from_mail'),
'MAIL_FROM_NAME' => $request->get('from_name'),
];
break;
case 'mailgun':
$oldMailData =
'MAIL_DRIVER='.config('mail.driver')."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
$mailFromCredential;
$newMailData =
'MAIL_DRIVER='.$request->mail_driver."\n".
'MAIL_HOST='.$request->mail_host."\n".
'MAIL_PORT='.$request->mail_port."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.$request->mail_encryption."\n\n".
'MAIL_FROM_ADDRESS='.$request->from_mail."\n".
'MAIL_FROM_NAME="'.$request->from_name."\"\n\n";
$extraMailData =
'MAILGUN_DOMAIN='.$request->mail_mailgun_domain."\n".
'MAILGUN_SECRET='.$request->mail_mailgun_secret."\n".
'MAILGUN_ENDPOINT='.$request->mail_mailgun_endpoint."\n";
if (env('MAILGUN_DOMAIN') !== null && env('MAILGUN_SECRET') !== null && env('MAILGUN_ENDPOINT') !== null) {
$extraOldMailData =
'MAILGUN_DOMAIN='.config('services.mailgun.domain')."\n".
'MAILGUN_SECRET='.config('services.mailgun.secret')."\n".
'MAILGUN_ENDPOINT='.config('services.mailgun.endpoint')."\n";
}
$mailEnv = [
'MAIL_DRIVER' => $request->get('mail_driver'),
'MAIL_HOST' => $request->get('mail_host'),
'MAIL_PORT' => $request->get('mail_port'),
'MAIL_USERNAME' => config('mail.username'),
'MAIL_PASSWORD' => config('mail.password'),
'MAIL_ENCRYPTION' => $request->get('mail_encryption'),
'MAIL_FROM_ADDRESS' => $request->get('from_mail'),
'MAIL_FROM_NAME' => $request->get('from_name'),
'MAILGUN_DOMAIN' => $request->get('mail_mailgun_domain'),
'MAILGUN_SECRET' => $request->get('mail_mailgun_secret'),
'MAILGUN_ENDPOINT' => $request->get('mail_mailgun_endpoint'),
];
break;
case 'ses':
$oldMailData =
'MAIL_DRIVER='.config('mail.driver')."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
$mailFromCredential;
$newMailData =
'MAIL_DRIVER='.$request->mail_driver."\n".
'MAIL_HOST='.$request->mail_host."\n".
'MAIL_PORT='.$request->mail_port."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.$request->mail_encryption."\n\n".
'MAIL_FROM_ADDRESS='.$request->from_mail."\n".
'MAIL_FROM_NAME="'.$request->from_name."\"\n\n";
$extraMailData =
'SES_KEY='.$request->mail_ses_key."\n".
'SES_SECRET='.$request->mail_ses_secret."\n";
if (env('SES_KEY') !== null && env('SES_SECRET') !== null) {
$extraOldMailData =
'SES_KEY='.config('services.ses.key')."\n".
'SES_SECRET='.config('services.ses.secret')."\n";
}
break;
case 'mail':
$oldMailData =
'MAIL_DRIVER='.config('mail.driver')."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
$mailFromCredential;
$newMailData =
'MAIL_DRIVER='.$request->mail_driver."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
'MAIL_FROM_ADDRESS='.$request->from_mail."\n".
'MAIL_FROM_NAME="'.$request->from_name."\"\n\n";
$mailEnv = [
'MAIL_DRIVER' => $request->get('mail_driver'),
'MAIL_HOST' => $request->get('mail_host'),
'MAIL_PORT' => $request->get('mail_port'),
'MAIL_USERNAME' => config('mail.username'),
'MAIL_PASSWORD' => config('mail.password'),
'MAIL_ENCRYPTION' => $request->get('mail_encryption'),
'MAIL_FROM_ADDRESS' => $request->get('from_mail'),
'MAIL_FROM_NAME' => $request->get('from_name'),
'SES_KEY' => $request->get('mail_ses_key'),
'SES_SECRET' => $request->get('mail_ses_secret'),
];
break;
case 'sendmail':
$oldMailData =
'MAIL_DRIVER='.config('mail.driver')."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
$mailFromCredential;
case 'mail':
$newMailData =
'MAIL_DRIVER='.$request->mail_driver."\n".
'MAIL_HOST='.config('mail.host')."\n".
'MAIL_PORT='.config('mail.port')."\n".
'MAIL_USERNAME='.config('mail.username')."\n".
'MAIL_PASSWORD='.config('mail.password')."\n".
'MAIL_ENCRYPTION='.config('mail.encryption')."\n\n".
'MAIL_FROM_ADDRESS='.$request->from_mail."\n".
'MAIL_FROM_NAME="'.$request->from_name."\"\n\n";
$mailEnv = [
'MAIL_DRIVER' => $request->get('mail_driver'),
'MAIL_HOST' => config('mail.host'),
'MAIL_PORT' => config('mail.port'),
'MAIL_USERNAME' => config('mail.username'),
'MAIL_PASSWORD' => config('mail.password'),
'MAIL_ENCRYPTION' => config('mail.encryption'),
'MAIL_FROM_ADDRESS' => $request->get('from_mail'),
'MAIL_FROM_NAME' => $request->get('from_name'),
];
break;
}
return [
'old_mail_data' => $oldMailData,
'new_mail_data' => $newMailData,
'extra_mail_data' => $extraMailData,
'extra_old_mail_data' => $extraOldMailData,
];
return $mailEnv;
}
/**
* Save the disk content to the .env file.
*
* @param Request $request
* @return array
*/
public function saveDiskVariables(DiskEnvironmentRequest $request)
{
$diskData = $this->getDiskData($request);
$diskEnv = $this->getDiskConfiguration($request);
try {
if (! $diskData['old_default_driver']) {
file_put_contents($this->envPath, $diskData['default_driver'], FILE_APPEND);
} else {
file_put_contents($this->envPath, str_replace(
$diskData['old_default_driver'],
$diskData['default_driver'],
file_get_contents($this->envPath)
));
}
if (! $diskData['old_disk_data']) {
file_put_contents($this->envPath, $diskData['new_disk_data'], FILE_APPEND);
} else {
file_put_contents($this->envPath, str_replace(
$diskData['old_disk_data'],
$diskData['new_disk_data'],
file_get_contents($this->envPath)
));
}
$this->updateEnv($diskEnv);
} catch (Exception $e) {
return [
'error' => 'disk_variables_save_error',
@@ -456,113 +317,75 @@ class EnvironmentManager
];
}
private function getDiskData($request)
/**
* Returns the disk configuration
*
* @return array
*/
private function getDiskConfiguration(DiskEnvironmentRequest $request)
{
$oldDefaultDriver = '';
$defaultDriver = '';
$oldDiskData = '';
$newDiskData = '';
$diskEnv = [];
if ($request->default_driver) {
if (env('FILESYSTEM_DRIVER') !== null) {
$defaultDriver = "\n".'FILESYSTEM_DRIVER='.$request->default_driver."\n";
$driver = $request->get('app_domain');
$oldDefaultDriver =
"\n".'FILESYSTEM_DRIVER='.config('filesystems.default')."\n";
} else {
$defaultDriver =
"\n".'FILESYSTEM_DRIVER='.$request->default_driver."\n";
}
if ($driver) {
$diskEnv['FILESYSTEM_DRIVER'] = $driver;
}
switch ($request->selected_driver) {
switch ($request->get('selected_driver')) {
case 's3':
if (env('AWS_KEY') !== null) {
$oldDiskData = "\n".
'AWS_KEY='.config('filesystems.disks.s3.key')."\n".
'AWS_SECRET="'.config('filesystems.disks.s3.secret')."\"\n".
'AWS_REGION='.config('filesystems.disks.s3.region')."\n".
'AWS_BUCKET='.config('filesystems.disks.s3.bucket')."\n".
'AWS_ROOT='.config('filesystems.disks.s3.root')."\n";
}
$newDiskData = "\n".
'AWS_KEY='.$request->aws_key."\n".
'AWS_SECRET="'.$request->aws_secret."\"\n".
'AWS_REGION='.$request->aws_region."\n".
'AWS_BUCKET='.$request->aws_bucket."\n".
'AWS_ROOT='.$request->aws_root."\n";
$diskEnv = [
'AWS_KEY' => $request->get('aws_key'),
'AWS_SECRET' => $request->get('aws_secret'),
'AWS_REGION' => $request->get('aws_region'),
'AWS_BUCKET' => $request->get('aws_bucket'),
'AWS_ROOT' => $request->get('aws_root'),
];
break;
case 'doSpaces':
if (env('DO_SPACES_KEY') !== null) {
$oldDiskData = "\n".
'DO_SPACES_KEY='.config('filesystems.disks.doSpaces.key')."\n".
'DO_SPACES_SECRET="'.config('filesystems.disks.doSpaces.secret')."\"\n".
'DO_SPACES_REGION='.config('filesystems.disks.doSpaces.region')."\n".
'DO_SPACES_BUCKET='.config('filesystems.disks.doSpaces.bucket')."\n".
'DO_SPACES_ENDPOINT='.config('filesystems.disks.doSpaces.endpoint')."\n";
'DO_SPACES_ROOT='.config('filesystems.disks.doSpaces.root')."\n";
}
$newDiskData = "\n".
'DO_SPACES_KEY='.$request->do_spaces_key."\n".
'DO_SPACES_SECRET="'.$request->do_spaces_secret."\"\n".
'DO_SPACES_REGION='.$request->do_spaces_region."\n".
'DO_SPACES_BUCKET='.$request->do_spaces_bucket."\n".
'DO_SPACES_ENDPOINT='.$request->do_spaces_endpoint."\n";
'DO_SPACES_ROOT='.$request->do_spaces_root."\n\n";
$diskEnv = [
'DO_SPACES_KEY' => $request->get('do_spaces_key'),
'DO_SPACES_SECRET' => $request->get('do_spaces_secret'),
'DO_SPACES_REGION' => $request->get('do_spaces_region'),
'DO_SPACES_BUCKET' => $request->get('do_spaces_bucket'),
'DO_SPACES_ENDPOINT' => $request->get('do_spaces_endpoint'),
'DO_SPACES_ROOT' => $request->get('do_spaces_root'),
];
break;
case 'dropbox':
if (env('DROPBOX_TOKEN') !== null) {
$oldDiskData = "\n".
'DROPBOX_TOKEN='.config('filesystems.disks.dropbox.token')."\n".
'DROPBOX_KEY='.config('filesystems.disks.dropbox.key')."\n".
'DROPBOX_SECRET="'.config('filesystems.disks.dropbox.secret')."\"\n".
'DROPBOX_APP='.config('filesystems.disks.dropbox.app')."\n".
'DROPBOX_ROOT='.config('filesystems.disks.dropbox.root')."\n";
}
$newDiskData = "\n".
'DROPBOX_TOKEN='.$request->dropbox_token."\n".
'DROPBOX_KEY='.$request->dropbox_key."\n".
'DROPBOX_SECRET="'.$request->dropbox_secret."\"\n".
'DROPBOX_APP='.$request->dropbox_app."\n".
'DROPBOX_ROOT='.$request->dropbox_root."\n";
$diskEnv = [
'DROPBOX_TOKEN' => $request->get('dropbox_token'),
'DROPBOX_KEY' => $request->get('dropbox_key'),
'DROPBOX_SECRET' => $request->get('dropbox_secret'),
'DROPBOX_APP' => $request->get('dropbox_app'),
'DROPBOX_ROOT' => $request->get('dropbox_root'),
];
break;
}
return [
'old_disk_data' => $oldDiskData,
'new_disk_data' => $newDiskData,
'default_driver' => $defaultDriver,
'old_default_driver' => $oldDefaultDriver,
];
return $diskEnv;
}
/**
* Save sanctum statful domain to the .env file.
* Save sanctum stateful domain to the .env file.
*
* @return array
*/
public function saveDomainVariables(DomainEnvironmentRequest $request)
{
try {
file_put_contents($this->envPath, str_replace(
'SANCTUM_STATEFUL_DOMAINS='.env('SANCTUM_STATEFUL_DOMAINS'),
'SANCTUM_STATEFUL_DOMAINS='.$request->app_domain,
file_get_contents($this->envPath)
));
file_put_contents($this->envPath, str_replace(
'SESSION_DOMAIN='.config('session.domain'),
'SESSION_DOMAIN='.explode(':', $request->app_domain)[0],
file_get_contents($this->envPath)
));
$this->updateEnv([
'SANCTUM_STATEFUL_DOMAINS' => $request->get('app_domain'),
'SESSION_DOMAIN' => explode(':', $request->get('app_domain'))[0],
]);
} catch (Exception $e) {
return [
'error' => 'domain_verification_failed',
@@ -573,4 +396,34 @@ class EnvironmentManager
'success' => 'domain_variable_save_successfully',
];
}
/**
* Order the env contents
*
* @return void
*/
public function reoderEnv()
{
$contents = $this->getEnvContents();
$contents = explode($this->delimiter, $contents);
if (empty($contents)) {
return;
}
natsort($contents);
$formatted = '';
$previous = '';
foreach ($contents as $current) {
$parts_line = explode('_', $current);
$parts_last = explode('_', $previous);
if ($parts_line[0] != $parts_last[0]) {
$formatted .= $this->delimiter;
}
$formatted .= $current.$this->delimiter;
$previous = $current;
}
file_put_contents($this->envPath, trim($formatted));
}
}

View File

@@ -2,6 +2,8 @@
namespace InvoiceShelf\Space;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Log;
use League\Flysystem\FilesystemException;
class InstallUtils
@@ -9,11 +11,11 @@ class InstallUtils
/**
* Check if database is created
*
* @return bool|int|string
* @return bool
*/
public static function isDbCreated()
{
return self::tableExists('users');
return self::dbMarkerExists() && self::tableExists('users');
}
/**
@@ -30,19 +32,14 @@ class InstallUtils
}
try {
$flag = self::dbMarkerExists();
if ($flag) {
$flag &= \Schema::hasTable($table);
}
} catch (\Illuminate\Database\QueryException $e) {
$flag = false;
} catch (\Exception $e) {
$flag = \Schema::hasTable($table);
} catch (QueryException|\Exception $e) {
$flag = false;
}
$cache[$table] = $flag;
return $flag;
return $cache[$table];
}
/**
@@ -53,34 +50,43 @@ class InstallUtils
public static function dbMarkerExists()
{
try {
$flag = \Storage::disk('local')->has('database_created');
return \Storage::disk('local')->has('database_created');
} catch (FilesystemException $e) {
$flag = false;
Log::error('Unable to verify db marker: '.$e->getMessage());
}
return $flag;
return false;
}
/**
* Creates the database marker
*
* @return void
* @return bool
*/
public static function createDbMarker()
{
\Storage::disk('local')->put('database_created', 'database_created');
try {
return \Storage::disk('local')->put('database_created', time());
} catch (\Exception $e) {
Log::error('Unable to create db marker: '.$e->getMessage());
}
return false;
}
/**
* Deletes the database marker
*
* @return void
* @return bool
*/
public static function deleteDbMarker()
{
try {
\Storage::disk('local')->delete('database_created');
return \Storage::disk('local')->delete('database_created');
} catch (\Exception $e) {
Log::error('Unable to delete db marker: '.$e->getMessage());
}
return false;
}
}

View File

@@ -6,11 +6,14 @@ trait GeneratesMenuTrait
{
public function generateMenu($key, $user)
{
$menu = [];
$new_items = [];
foreach (\Menu::get($key)->items->toArray() as $data) {
$menu = \Menu::get($key);
$items = $menu ? $menu->items->toArray() : [];
foreach ($items as $data) {
if ($user->checkAccess($data)) {
$menu[] = [
$new_items[] = [
'title' => $data->title,
'link' => $data->link->path['url'],
'icon' => $data->data['icon'],
@@ -20,6 +23,6 @@ trait GeneratesMenuTrait
}
}
return $menu;
return $new_items;
}
}

View File

@@ -9,22 +9,23 @@
"type": "project",
"require": {
"php": "^8.1",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.3",
"laravel/tinker": "^2.8",
"laravel/helpers": "^1.5",
"laravel/ui": "^4.4",
"ext-json": "*",
"aws/aws-sdk-php": "^3.297",
"barryvdh/laravel-dompdf": "^v2.0",
"invoiceshelf/modules": "^1.0.0",
"doctrine/dbal": "^3.8",
"dragonmantank/cron-expression": "^3.3",
"guzzlehttp/guzzle": "^7.2",
"invoiceshelf/modules": "^1.0.0",
"jasonmccreary/laravel-test-assertions": "^2.3",
"laravel/framework": "^10.10",
"laravel/helpers": "^1.5",
"laravel/sanctum": "^3.3",
"laravel/tinker": "^2.8",
"laravel/ui": "^4.4",
"lavary/laravel-menu": "^1.8",
"league/flysystem-aws-s3-v3": "^3.23",
"predis/predis": "^2.2",
"silber/bouncer": "^v1.0",
"silber/bouncer": "v1.0.1",
"spatie/flysystem-dropbox": "^3.0",
"spatie/laravel-backup": "^8.5",
"spatie/laravel-medialibrary": "^10.15",
@@ -38,7 +39,7 @@
"nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.1",
"spatie/laravel-ignition": "^2.0",
"barryvdh/laravel-ide-helper": "^2.13",
"barryvdh/laravel-ide-helper": "v3.0.0",
"beyondcode/laravel-dump-server": "^1.9",
"pestphp/pest": "^2.33",
"pestphp/pest-plugin-faker": "^2.0",

2237
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,16 @@ return [
'root' => env('AWS_ROOT'),
],
's3compat' => [
'driver' => 's3',
'endpoint' => env('S3_COMPAT_ENDPOINT'),
'use_path_style_endpoint' => true,
'key' => env('S3_COMPAT_KEY'),
'secret' => env('S3_COMPAT_SECRET'),
'region' => env('S3_COMPAT_REGION'),
'bucket' => env('S3_COMPAT_BUCKET'),
],
'media' => [
'driver' => 'local',
'root' => public_path('media'),

View File

@@ -39,8 +39,8 @@ return [
'resources/sass/module' => 'Resources/sass/module.scss',
'resources/scripts/stores/sample-store' => 'Resources/scripts/stores/sample-store.js',
'resources/scripts/views/SamplePage' => 'Resources/scripts/views/SamplePage.vue',
'resources/locales/en' => 'Resources/locales/en.json',
'resources/locales/locales' => 'Resources/locales/locales.js',
'lang/en' => 'Lang/en.json',
'lang/locales' => 'Lang/locales.js',
'package' => 'package.json',
'postcss.config' => 'postcss.config.js',
'tailwind.config' => 'tailwind.config.js',

View File

@@ -1,3 +1,3 @@
files:
- source: /resources/scripts/locales/en.json
translation: /resources/scripts/locales/%two_letters_code%.json
- source: /lang/en.json
translation: /lang/%two_letters_code%.json

View File

@@ -24,8 +24,5 @@ class AddUserIdToExpensesTable extends Migration
*
* @return void
*/
public function down()
{
}
public function down() {}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class TaxesAmountAsSigned extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('taxes', function (Blueprint $table) {
$table->bigInteger('amount')->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('taxes', function (Blueprint $table) {
$table->unsignedBigInteger('amount')->change();
});
}
}

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use InvoiceShelf\Models\Setting;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
try {
DB::update("UPDATE abilities SET entity_type = REPLACE(entity_type, 'Crater', 'InvoiceShelf')");
DB::update("UPDATE assigned_roles SET entity_type = REPLACE(entity_type, 'Crater', 'InvoiceShelf')");
} catch (\Exception $e) {
}
Setting::setSetting('version', '1.1.0');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
try {
DB::update("UPDATE abilities SET entity_type = REPLACE(entity_type, 'InvoiceShelf', 'Crater')");
DB::update("UPDATE assigned_roles SET entity_type = REPLACE(entity_type, 'InvoiceShelf', 'Crater')");
} catch (\Exception $e) {
}
Setting::setSetting('version', '1.0.0');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('personal_access_tokens', function (Blueprint $table) {
$table->dateTime('expires_at')->nullable()->after('last_used_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('personal_access_tokens', function (Blueprint $table) {
$table->dropColumn('expires_at');
});
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('companies', function (Blueprint $table) {
$table->after('unique_hash', function (Blueprint $table) {
$table->string('vat_id')->nullable();
$table->string('tax_id')->nullable();
});
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('companies', function (Blueprint $table) {
$table->dropColumn('vat_id');
$table->dropColumn('tax_id');
});
}
};

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
try {
DB::update("UPDATE media SET model_type = REPLACE(model_type, 'Crater', 'InvoiceShelf')");
DB::update("UPDATE email_logs SET mailable_type = REPLACE(model_type, 'Crater', 'InvoiceShelf')");
DB::update("UPDATE notifications SET notifiable_type = REPLACE(notifiable_type, 'Crater', 'InvoiceShelf')");
DB::update("UPDATE personal_access_tokens SET tokenable_type = REPLACE(tokenable_type, 'Crater', 'InvoiceShelf')");
DB::update("UPDATE custom_field_values SET custom_field_valuable_type = REPLACE(custom_field_valuable_type, 'Crater', 'InvoiceShelf')");
} catch (\Exception $e) {
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
try {
DB::update("UPDATE media SET model_type = REPLACE(model_type, 'InvoiceShelf', 'Crater')");
DB::update("UPDATE email_logs SET mailable_type = REPLACE(model_type, 'InvoiceShelf', 'Crater')");
DB::update("UPDATE notifications SET notifiable_type = REPLACE(notifiable_type, 'InvoiceShelf', 'Crater')");
DB::update("UPDATE personal_access_tokens SET tokenable_type = REPLACE(tokenable_type, 'InvoiceShelf', 'Crater')");
DB::update("UPDATE custom_field_values SET custom_field_valuable_type = REPLACE(custom_field_valuable_type, 'InvoiceShelf', 'Crater')");
} catch (\Exception $e) {
}
}
};

View File

@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use InvoiceShelf\Models\Setting;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Setting::setSetting('version', '1.2.1');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Setting::setSetting('version', '1.2.0');
}
};

View File

@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use InvoiceShelf\Models\Setting;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Setting::setSetting('version', '1.2.2');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Setting::setSetting('version', '1.2.1');
}
};

View File

@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use InvoiceShelf\Models\Setting;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Setting::setSetting('version', '1.3.0');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Setting::setSetting('version', '1.2.2');
}
};

View File

@@ -616,6 +616,14 @@ class CurrenciesTableSeeder extends Seeder
'decimal_separator' => ',',
'swap_currency_symbol' => true,
],
[
'name' => 'Libyan Dinar',
'code' => 'LYD',
'symbol' => 'LD',
'precision' => '3',
'thousand_separator' => ',',
'decimal_separator' => '.',
],
];
foreach ($currencies as $currency) {

View File

@@ -863,6 +863,8 @@
"company_info": {
"company_info": "Firmeninfo",
"company_name": "Name des Unternehmens",
"vat_id": "Umsatzsteuer-Identifikationsnummer",
"tax_id": "Steuernummer",
"company_logo": "Firmenlogo",
"section_description": "Informationen zu Ihrem Unternehmen, die auf Rechnungen, Angeboten und anderen von InvoiceShelf erstellten Dokumenten angezeigt werden.",
"phone": "Telefon",
@@ -1267,6 +1269,12 @@
"aws_region": "AWS-Region",
"aws_bucket": "AWS Bucket",
"aws_root": "AWS-Pfad",
"s3_endpoint": "S3 Endpunkt",
"s3_key": "S3 Schlüssel",
"s3_secret": "S3 Geheimnis",
"s3_region": "S3 Region",
"s3_bucket": "S3 Bucket",
"s3_root": "S3 Pfad",
"do_spaces_type": "Do Spaces-Typ",
"do_spaces_key": "Do Spaces Key",
"do_spaces_secret": "Do Spaces Secret",
@@ -1522,5 +1530,7 @@
"pdf_bill_to": "Rechnungsanschrift",
"pdf_ship_to": "Lieferanschrift",
"pdf_received_from": "Erhalten von:",
"pdf_tax_label": "Steuer"
"pdf_tax_label": "Steuer",
"pdf_tax_id": "Steuer-Nr.",
"pdf_vat_id": "USt.-ID"
}

View File

@@ -863,6 +863,8 @@
"company_info": {
"company_info": "Company info",
"company_name": "Company Name",
"tax_id": "Tax Identification Number",
"vat_id": "VAT Identification Number",
"company_logo": "Company Logo",
"section_description": "Information about your company that will be displayed on invoices, estimates and other documents created by InvoiceShelf.",
"phone": "Phone",
@@ -1267,6 +1269,12 @@
"aws_region": "AWS Region",
"aws_bucket": "AWS Bucket",
"aws_root": "AWS Root",
"s3_endpoint": "S3 Endpoint",
"s3_key": "S3 Key",
"s3_secret": "S3 Secret",
"s3_region": "S3 Region",
"s3_bucket": "S3 Bucket",
"s3_root": "S3 Root",
"do_spaces_type": "Do Spaces type",
"do_spaces_key": "Do Spaces key",
"do_spaces_secret": "Do Spaces Secret",
@@ -1522,5 +1530,7 @@
"pdf_bill_to": "Bill to,",
"pdf_ship_to": "Ship to,",
"pdf_received_from": "Received from:",
"pdf_tax_label": "Tax"
"pdf_tax_label": "Tax",
"pdf_tax_id": "Tax-ID",
"pdf_vat_id": "VAT-ID"
}

View File

@@ -1,4 +1,4 @@
<img src="https://github.com/InvoiceShelf/InvoiceShelf/assets/5760249/70c0ea3a-b497-4d9a-96f5-b15d112d6f59">
<img src="https://github.com/user-attachments/assets/a6ae2080-e865-4fde-b41d-5a09964d7de2">
## Introduction
@@ -41,7 +41,7 @@ The Web Application is made using Laravel & VueJS while the Mobile Apps are buil
## Discord
Join the InvoiceShelf discord server to discuss:
[Invite Link](https://discord.gg/KMpTGTpk)
[Invite Link](https://discord.gg/eHXf4zWhsR)
## Roadmap

View File

@@ -1,7 +1,7 @@
import { createApp } from 'vue'
import App from '@/scripts/App.vue'
import { createI18n } from 'vue-i18n'
import messages from '@/scripts/locales/locales'
import messages from '/lang/locales'
import router from '@/scripts/router/index'
import { defineGlobalComponents } from './global-components'
import utils from '@/scripts/helpers/utilities.js'

View File

@@ -271,23 +271,19 @@ const price = computed({
} else {
updateItemAttribute('price', newValue)
}
setDiscount()
},
})
const subtotal = computed(() => props.itemData.price * props.itemData.quantity)
const subtotal = computed(() => Math.round(props.itemData.price * props.itemData.quantity))
const discount = computed({
get: () => {
return props.itemData.discount
},
set: (newValue) => {
if (props.itemData.discount_type === 'percentage') {
updateItemAttribute('discount_val', (subtotal.value * newValue) / 100)
} else {
updateItemAttribute('discount_val', Math.round(newValue * 100))
}
updateItemAttribute('discount', newValue)
setDiscount()
},
})
@@ -313,7 +309,7 @@ const showRemoveButton = computed(() => {
const totalSimpleTax = computed(() => {
return Math.round(
sumBy(props.itemData.taxes, function (tax) {
if (!tax.compound_tax) {
if (tax.amount) {
return tax.amount
}
return 0
@@ -321,18 +317,7 @@ const totalSimpleTax = computed(() => {
)
})
const totalCompoundTax = computed(() => {
return Math.round(
sumBy(props.itemData.taxes, function (tax) {
if (tax.compound_tax) {
return tax.amount
}
return 0
})
)
})
const totalTax = computed(() => totalSimpleTax.value + totalCompoundTax.value)
const totalTax = computed(() => totalSimpleTax.value)
const rules = {
name: {
@@ -399,7 +384,7 @@ const v$ = useVuelidate(
function updateTax(data) {
props.store.$patch((state) => {
state[props.storeProp].items[props.index]['taxes'][data.index] = data.item
state[props.storeProp].items[props.index]['taxes'][data.index] = data.item
})
let lastTax = props.itemData.taxes[props.itemData.taxes.length - 1]
@@ -416,6 +401,16 @@ function updateTax(data) {
syncItemToStore()
}
function setDiscount() {
const newValue = props.store[props.storeProp].items[props.index].discount
if (props.itemData.discount_type === 'percentage'){
updateItemAttribute('discount_val', Math.round((subtotal.value * newValue) / 100))
}else{
updateItemAttribute('discount_val', Math.round(newValue * 100))
}
}
function searchVal(val) {
updateItemAttribute('name', val)
}
@@ -485,10 +480,12 @@ function syncItemToStore() {
total: total.value,
sub_total: subtotal.value,
totalSimpleTax: totalSimpleTax.value,
totalCompoundTax: totalCompoundTax.value,
totalTax: totalTax.value,
tax: totalTax.value,
taxes: [...itemTaxes],
tax_type_ids: itemTaxes.flatMap(_t =>
_t.tax_type_id ? _t.tax_type_id : [],
),
}
props.store.updateItem(data)

View File

@@ -146,14 +146,14 @@ const filteredTypes = computed(() => {
})
const taxAmount = computed(() => {
if (localTax.compound_tax && props.discountedTotal) {
return ((props.discountedTotal + props.totalTax) * localTax.percent) / 100
}
if (props.discountedTotal && localTax.percent) {
const taxPerItemEnabled = props.store[props.storeProp].tax_per_item === 'YES'
const discountPerItemEnabled = props.store[props.storeProp].discount_per_item === 'YES'
if (taxPerItemEnabled && !discountPerItemEnabled){
return getTaxAmount()
}
return (props.discountedTotal * localTax.percent) / 100
}
return 0
})
@@ -171,6 +171,13 @@ watch(
}
)
watch(
() => taxAmount.value,
() => {
updateRowTax()
},
)
// Set SelectedTax
if (props.taxData.tax_type_id > 0) {
selectedTax.value = taxTypeStore.taxTypes.find(
@@ -183,7 +190,6 @@ updateRowTax()
function onSelectTax(val) {
localTax.percent = val.percent
localTax.tax_type_id = val.id
localTax.compound_tax = val.compound_tax
localTax.name = val.name
updateRowTax()
@@ -220,6 +226,27 @@ function openTaxModal() {
function removeTax(index) {
props.store.$patch((state) => {
state[props.storeProp].items[props.itemIndex].taxes.splice(index, 1)
state[props.storeProp].items[props.itemIndex].tax = 0
state[props.storeProp].items[props.itemIndex].totalTax = 0
})
}
function getTaxAmount() {
let total = 0
let discount = 0
const itemTotal = props.discountedTotal
const modelDiscount = props.store[props.storeProp].discount ? props.store[props.storeProp].discount : 0
const type = props.store[props.storeProp].discount_type
if (modelDiscount > 0) {
props.store[props.storeProp].items.forEach((_i) => {
total += _i.total
})
const proportion = (itemTotal / total).toFixed(2)
discount = type === 'fixed' ? modelDiscount * 100 : (total * modelDiscount) / 100
const itemDiscount = Math.round(discount * proportion)
const discounted = itemTotal - itemDiscount
return Math.round((discounted * localTax.percent) / 100)
}
return Math.round((props.discountedTotal * localTax.percent) / 100)
}
</script>

View File

@@ -191,7 +191,7 @@
</template>
<script setup>
import { computed, inject, ref } from 'vue'
import { computed, inject, ref, watch } from 'vue'
import Guid from 'guid'
import Tax from './CreateTotalTaxes.vue'
import TaxStub from '@/scripts/admin/stub/abilities'
@@ -227,19 +227,20 @@ const utils = inject('$utils')
const companyStore = useCompanyStore()
watch(
() => props.store[props.storeProp].items,
(val) => {
setDiscount()
}, { deep: true },
)
const totalDiscount = computed({
get: () => {
return props.store[props.storeProp].discount
},
set: (newValue) => {
if (props.store[props.storeProp].discount_type === 'percentage') {
props.store[props.storeProp].discount_val = Math.round(
(props.store.getSubTotal * newValue) / 100
)
} else {
props.store[props.storeProp].discount_val = Math.round(newValue * 100)
}
props.store[props.storeProp].discount = newValue
setDiscount()
},
})
@@ -265,7 +266,7 @@ const itemWiseTaxes = computed(() => {
} else if (tax.tax_type_id) {
taxes.push({
tax_type_id: tax.tax_type_id,
amount: tax.amount,
amount: Math.round(tax.amount),
percent: tax.percent,
name: tax.name,
})
@@ -284,6 +285,19 @@ const defaultCurrency = computed(() => {
}
})
function setDiscount() {
const newValue = props.store[props.storeProp].discount
if (props.store[props.storeProp].discount_type === 'percentage') {
props.store[props.storeProp].discount_val
= Math.round((props.store.getSubTotal * newValue) / 100)
return
}
props.store[props.storeProp].discount_val = Math.round(newValue * 100)
}
function selectFixed() {
if (props.store[props.storeProp].discount_type === 'fixed') {
return
@@ -295,24 +309,21 @@ function selectFixed() {
}
function selectPercentage() {
if (props.store[props.storeProp].discount_type === 'percentage') {
if (props.store[props.storeProp].discount_type === 'percentage'){
return
}
props.store[props.storeProp].discount_val =
(props.store.getSubTotal * props.store[props.storeProp].discount) / 100
const val = Math.round(props.store[props.storeProp].discount * 100) / 100
props.store[props.storeProp].discount_val
= Math.round((props.store.getSubTotal * val) / 100)
props.store[props.storeProp].discount_type = 'percentage'
}
function onSelectTax(selectedTax) {
let amount = 0
if (selectedTax.compound_tax && props.store.getSubtotalWithDiscount) {
amount = Math.round(
((props.store.getSubtotalWithDiscount + props.store.getTotalSimpleTax) *
selectedTax.percent) /
100
)
} else if (props.store.getSubtotalWithDiscount && selectedTax.percent) {
if (props.store.getSubtotalWithDiscount && selectedTax.percent) {
amount = Math.round(
(props.store.getSubtotalWithDiscount * selectedTax.percent) / 100
)
@@ -323,7 +334,6 @@ function onSelectTax(selectedTax) {
id: Guid.raw(),
name: selectedTax.name,
percent: selectedTax.percent,
compound_tax: selectedTax.compound_tax,
tax_type_id: selectedTax.id,
amount,
}

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