19 Commits
1.1.0 ... 1.2.0

Author SHA1 Message Date
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
106 changed files with 1078 additions and 400 deletions

View 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

@@ -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

@@ -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

@@ -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

@@ -64,8 +64,6 @@ class EnvironmentManager
if ($entry[0] == $data_key) {
$env[$env_key] = $data_key.'='.$this->encode($data_value);
$updated = true;
} else {
$env[$env_key] = $this->encode($env_value);
}
}
@@ -90,7 +88,7 @@ class EnvironmentManager
private function encode($str)
{
if (strpos($str, ' ') !== false || preg_match('/'.preg_quote('^\'£$%^&*()}{@#~?><,@|-=-_+-¬', '/').'/', $str)) {
if ((strpos($str, ' ') !== false || preg_match('/'.preg_quote('^\'£$%^&*()}{@#~?><,@|-=-_+-¬', '/').'/', $str)) && ($str[0] != '"' || $str[strlen($str) - 1] != '"')) {
$str = '"'.$str.'"';
}

View File

@@ -24,7 +24,7 @@
"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",

459
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

@@ -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

@@ -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,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,
}

View File

@@ -21,13 +21,7 @@
>
<template #default="slotProps">
<div
class="
z-0
flex
justify-end
p-4
border-t border-solid border-gray-light
"
class="z-0 flex justify-end p-4 border-t border-solid border-gray-light"
>
<BaseButton
class="mr-3 text-sm"
@@ -66,12 +60,14 @@ import { computed, ref, watchEffect } from 'vue'
import Dropbox from '@/scripts/admin/components/modal-components/disks/DropboxDisk.vue'
import Local from '@/scripts/admin/components/modal-components/disks/LocalDisk.vue'
import S3 from '@/scripts/admin/components/modal-components/disks/S3Disk.vue'
import S3compat from '@/scripts/admin/components/modal-components/disks/S3CompatDisk.vue'
import DoSpaces from '@/scripts/admin/components/modal-components/disks/DoSpacesDisk.vue'
export default {
components: {
Dropbox,
Local,
S3,
S3compat,
DoSpaces,
},
setup() {
@@ -109,20 +105,28 @@ export default {
}
async function createNewDisk(data) {
Object.assign(diskStore.diskConfigData, data)
isLoading.value = true
try {
Object.assign(diskStore.diskConfigData, data)
isLoading.value = true
let formData = {
id: modalStore.id,
...data,
let formData = {
id: modalStore.id,
...data,
}
let response = null
const action = isEdit.value
? diskStore.updateDisk
: diskStore.createDisk
response = await action(formData)
modalStore.refreshData()
closeDiskModal()
} catch (e) {
// error is handled by the disk store
} finally {
isLoading.value = false
}
let response = null
const action = isEdit.value ? diskStore.updateDisk : diskStore.createDisk
response = await action(formData)
isLoading.value = false
modalStore.refreshData()
closeDiskModal()
}
function closeDiskModal() {

View File

@@ -77,17 +77,6 @@
@input="v$.currentTaxType.description.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('tax_types.compound_tax')"
variant="horizontal"
class="flex flex-row-reverse"
>
<BaseSwitch
v-model="taxTypeStore.currentTaxType.compound_tax"
class="flex items-center"
/>
</BaseInputGroup>
</BaseInputGrid>
</div>
<div
@@ -209,14 +198,7 @@ async function submitTaxTypeData() {
function SelectTax(taxData) {
let amount = 0
if (taxData.compound_tax && estimateStore.getSubtotalWithDiscount) {
amount = Math.round(
((estimateStore.getSubtotalWithDiscount +
estimateStore.getTotalSimpleTax) *
taxData.percent) /
100
)
} else if (estimateStore.getSubtotalWithDiscount && taxData.percent) {
if (estimateStore.getSubtotalWithDiscount && taxData.percent) {
amount = Math.round(
(estimateStore.getSubtotalWithDiscount * taxData.percent) / 100
)
@@ -226,7 +208,6 @@ function SelectTax(taxData) {
id: Guid.raw(),
name: taxData.name,
percent: taxData.percent,
compound_tax: taxData.compound_tax,
tax_type_id: taxData.id,
amount,
}
@@ -242,7 +223,6 @@ function selectItemTax(taxData) {
id: Guid.raw(),
name: taxData.name,
percent: taxData.percent,
compound_tax: taxData.compound_tax,
tax_type_id: taxData.id,
}
modalStore.refreshData(data)

View File

@@ -0,0 +1,325 @@
<template>
<form @submit.prevent="submitData">
<div class="px-8 py-6">
<BaseInputGrid>
<BaseInputGroup
:label="$t('settings.disk.name')"
:error="
v$.s3CompatDiskConfigData.name.$error &&
v$.s3CompatDiskConfigData.name.$errors[0].$message
"
required
>
<BaseInput
v-model="diskStore.s3CompatDiskConfigData.name"
type="text"
name="name"
:invalid="v$.s3CompatDiskConfigData.name.$error"
@input="v$.s3CompatDiskConfigData.name.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.driver')"
:error="
v$.s3CompatDiskConfigData.selected_driver.$error &&
v$.s3CompatDiskConfigData.selected_driver.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="selected_driver"
:invalid="v$.s3CompatDiskConfigData.selected_driver.$error"
value-prop="value"
:options="disks"
searchable
label="name"
:can-deselect="false"
@update:modelValue="onChangeDriver(data)"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.s3_endpoint')"
:error="
v$.s3CompatDiskConfigData.root.$error &&
v$.s3CompatDiskConfigData.root.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="diskStore.s3CompatDiskConfigData.endpoint"
type="url"
name="endpoint"
placeholder="http://127.0.0.1:9005"
:invalid="v$.s3CompatDiskConfigData.endpoint.$error"
@input="v$.s3CompatDiskConfigData.endpoint.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.s3_root')"
:error="
v$.s3CompatDiskConfigData.root.$error &&
v$.s3CompatDiskConfigData.root.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="diskStore.s3CompatDiskConfigData.root"
type="text"
name="root"
placeholder="Ex. /user/root/"
:invalid="v$.s3CompatDiskConfigData.root.$error"
@input="v$.s3CompatDiskConfigData.root.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.s3_key')"
:error="
v$.s3CompatDiskConfigData.key.$error &&
v$.s3CompatDiskConfigData.key.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="diskStore.s3CompatDiskConfigData.key"
type="text"
name="key"
placeholder="Ex. KEIS4S39SERSDS"
:invalid="v$.s3CompatDiskConfigData.key.$error"
@input="v$.s3CompatDiskConfigData.key.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.s3_secret')"
:error="
v$.s3CompatDiskConfigData.secret.$error &&
v$.s3CompatDiskConfigData.secret.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="diskStore.s3CompatDiskConfigData.secret"
type="text"
name="secret"
placeholder="Ex. ********"
:invalid="v$.s3CompatDiskConfigData.secret.$error"
@input="v$.s3CompatDiskConfigData.secret.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.s3_region')"
:error="
v$.s3CompatDiskConfigData.region.$error &&
v$.s3CompatDiskConfigData.region.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="diskStore.s3CompatDiskConfigData.region"
type="text"
name="region"
placeholder="Ex. us-west"
:invalid="v$.s3CompatDiskConfigData.region.$error"
@input="v$.s3CompatDiskConfigData.region.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.s3_bucket')"
:error="
v$.s3CompatDiskConfigData.bucket.$error &&
v$.s3CompatDiskConfigData.bucket.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="diskStore.s3CompatDiskConfigData.bucket"
type="text"
name="bucket"
placeholder="Ex. AppName"
:invalid="v$.s3CompatDiskConfigData.bucket.$error"
@input="v$.s3CompatDiskConfigData.bucket.$touch()"
/>
</BaseInputGroup>
</BaseInputGrid>
<div v-if="!isDisabled" class="flex items-center mt-6">
<div class="relative flex items-center w-12">
<BaseSwitch v-model="set_as_default" class="flex" />
</div>
<div class="ml-4 right">
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
{{ $t('settings.disk.is_default') }}
</p>
</div>
</div>
</div>
<slot :disk-data="{ isLoading, submitData }" />
</form>
</template>
<script>
import { useDiskStore } from '@/scripts/admin/stores/disk'
import { useModalStore } from '@/scripts/stores/modal'
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import useVuelidate from '@vuelidate/core'
import { required, helpers } from '@vuelidate/validators'
export default {
props: {
isEdit: {
type: Boolean,
require: true,
default: false,
},
loading: {
type: Boolean,
require: true,
default: false,
},
disks: {
type: Array,
require: true,
default: Array,
},
},
emits: ['submit', 'onChangeDisk'],
setup(props, { emit }) {
const diskStore = useDiskStore()
const modalStore = useModalStore()
const { t } = useI18n()
let set_as_default = ref(false)
let isLoading = ref(false)
let selected_disk = ref(null)
let is_current_disk = ref(null)
const selected_driver = computed({
get: () => diskStore.selected_driver,
set: (value) => {
diskStore.selected_driver = value
diskStore.s3CompatDiskConfigData.selected_driver = value
},
})
const rules = computed(() => {
return {
s3CompatDiskConfigData: {
name: {
required: helpers.withMessage(t('validation.required'), required),
},
endpoint: {
required: helpers.withMessage(t('validation.required'), required),
},
root: {
required: helpers.withMessage(t('validation.required'), required),
},
key: {
required: helpers.withMessage(t('validation.required'), required),
},
secret: {
required: helpers.withMessage(t('validation.required'), required),
},
region: {
required: helpers.withMessage(t('validation.required'), required),
},
bucket: {
required: helpers.withMessage(t('validation.required'), required),
},
selected_driver: {
required: helpers.withMessage(t('validation.required'), required),
},
},
}
})
const v$ = useVuelidate(
rules,
computed(() => diskStore),
)
onBeforeUnmount(() => {
diskStore.s3CompatDiskConfigData = {
name: null,
selected_driver: 's3compat',
key: null,
secret: null,
region: null,
bucket: null,
root: null,
}
})
loadData()
async function loadData() {
isLoading.value = true
let data = reactive({
disk: 's3compat',
})
if (props.isEdit) {
Object.assign(diskStore.s3CompatDiskConfigData, modalStore.data)
set_as_default.value = modalStore.data.set_as_default
if (set_as_default.value) {
is_current_disk.value = true
}
} else {
let diskData = await diskStore.fetchDiskEnv(data)
Object.assign(diskStore.s3CompatDiskConfigData, diskData.data)
}
selected_disk.value = props.disks.find((v) => v.value == 's3compat')
isLoading.value = false
}
const isDisabled = computed(() => {
return props.isEdit && set_as_default.value && is_current_disk.value
? true
: false
})
async function submitData() {
v$.value.s3CompatDiskConfigData.$touch()
if (v$.value.s3CompatDiskConfigData.$invalid) {
return true
}
let data = {
credentials: diskStore.s3CompatDiskConfigData,
name: diskStore.s3CompatDiskConfigData.name,
driver: selected_disk.value.value,
set_as_default: set_as_default.value,
}
emit('submit', data)
return false
}
function onChangeDriver() {
emit('onChangeDisk', diskStore.s3CompatDiskConfigData.selected_driver)
}
return {
v$,
diskStore,
modalStore,
set_as_default,
isLoading,
selected_disk,
selected_driver,
is_current_disk,
loadData,
submitData,
onChangeDriver,
isDisabled,
}
},
}
</script>

View File

@@ -51,6 +51,17 @@ export const useDiskStore = (useWindow = false) => {
bucket: '',
root: '',
},
s3CompatDiskConfigData: {
name: '',
selected_driver: 's3compat',
key: '',
secret: '',
region: '',
bucket: '',
root: '',
endpoint: '',
},
}),
getters: {

View File

@@ -143,7 +143,8 @@ export const useEstimateStore = (useWindow = false) => {
axios
.get(`/api/v1/estimates/${id}`)
.then((response) => {
Object.assign(this.newEstimate, response.data.data)
this.setEstimateData(response.data.data)
this.setCustomerAddresses(this.newEstimate.customer)
resolve(response)
})
.catch((err) => {
@@ -154,6 +155,41 @@ export const useEstimateStore = (useWindow = false) => {
})
},
setEstimateData(estimate) {
Object.assign(this.newEstimate, estimate)
if (this.newEstimate.tax_per_item === 'YES') {
this.newEstimate.items.forEach((_i) => {
if (_i.taxes && !_i.taxes.length){
_i.taxes.push({ ...taxStub, id: Guid.raw() })
}
})
}
if (this.newEstimate.discount_per_item === 'YES') {
this.newEstimate.items.forEach((_i, index) => {
if (_i.discount_type === 'fixed'){
this.newEstimate.items[index].discount = _i.discount / 100
}
})
}
else {
if (this.newEstimate.discount_type === 'fixed'){
this.newEstimate.discount = this.newEstimate.discount / 100
}
}
},
setCustomerAddresses(customer) {
const customer_business = customer.customer_business
if (customer_business?.billing_address){
this.newEstimate.customer.billing_address = customer_business.billing_address
}
if (customer_business?.shipping_address){
this.newEstimate.customer.shipping_address = customer_business.shipping_address
}
},
addSalesTaxUs() {
const taxTypeStore = useTaxTypeStore()
let salesTax = { ...taxStub }

View File

@@ -79,8 +79,10 @@ export const useGlobalStore = (useWindow = false) => {
companyStore.selectedCompanyCurrency =
response.data.current_company_currency
global.locale =
response.data.current_user_settings.language || 'en'
if(typeof global.locale !== 'string') {
global.locale.value =
response.data.current_user_settings.language || 'en'
}
this.isAppLoaded = true
resolve(response)

View File

@@ -133,8 +133,8 @@ export const useInvoiceStore = (useWindow = false) => {
axios
.get(`/api/v1/invoices/${id}`)
.then((response) => {
Object.assign(this.newInvoice, response.data.data)
this.newInvoice.customer = response.data.data.customer
this.setInvoiceData(response.data.data)
this.setCustomerAddresses(this.newInvoice.customer)
resolve(response)
})
.catch((err) => {
@@ -144,6 +144,38 @@ export const useInvoiceStore = (useWindow = false) => {
})
},
setInvoiceData(invoice) {
Object.assign(this.newInvoice, invoice)
if (this.newInvoice.tax_per_item === 'YES') {
this.newInvoice.items.forEach((_i) => {
if (_i.taxes && !_i.taxes.length)
_i.taxes.push({ ...taxStub, id: Guid.raw() })
})
}
if (this.newInvoice.discount_per_item === 'YES') {
this.newInvoice.items.forEach((_i, index) => {
if (_i.discount_type === 'fixed')
this.newInvoice.items[index].discount = _i.discount / 100
})
}
else {
if (this.newInvoice.discount_type === 'fixed')
this.newInvoice.discount = this.newInvoice.discount / 100
}
},
setCustomerAddresses(customer) {
const customer_business = customer.customer_business
if (customer_business?.billing_address)
this.newInvoice.customer.billing_address = customer_business.billing_address
if (customer_business?.shipping_address)
this.newInvoice.customer.shipping_address = customer_business.shipping_address
},
addSalesTaxUs() {
const taxTypeStore = useTaxTypeStore()
let salesTax = { ...taxStub }

View File

@@ -106,7 +106,7 @@ export const useUserStore = (useWindow = false) => {
.then((response) => {
if (data.settings.language) {
this.currentUserSettings.language = data.settings.language
global.locale = data.settings.language
global.locale.value = data.settings.language
}
if (data.settings.default_estimate_template) {
this.currentUserSettings.default_estimate_template =

View File

@@ -138,6 +138,7 @@
<script setup>
import { computed, ref, watch, onMounted } from 'vue'
import { cloneDeep } from 'lodash'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import {
@@ -257,11 +258,30 @@ async function submitForm() {
isSaving.value = true
let data = {
let data = cloneDeep({
...estimateStore.newEstimate,
sub_total: estimateStore.getSubTotal,
total: estimateStore.getTotal,
tax: estimateStore.getTotalTax,
})
if (data.discount_per_item === 'YES') {
data.items.forEach((item, index) => {
if (item.discount_type === 'fixed'){
data.items[index].discount = Math.round(item.discount * 100)
}
})
}
else {
if (data.discount_type === 'fixed'){
data.discount = Math.round(data.discount * 100)
}
}
if (
!estimateStore.newEstimate.tax_per_item === 'YES'
&& data.taxes.length
){
data.tax_type_ids = data.taxes.map(_t => _t.tax_type_id)
}
const action = isEdit.value

View File

@@ -131,6 +131,25 @@
/>
</BaseInputGroup>
</div>
<BaseInputGroup :label="$t('settings.company_info.tax_id')">
<BaseInput
v-model.trim="companyForm.tax_id"
type="text"
name="tax_id"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.company_info.vat_id')"
class="mt-4"
>
<BaseInput
v-model.trim="companyForm.vat_id"
type="text"
name="vat_id"
/>
</BaseInputGroup>
</div>
<BaseButton :loading="isSaving" :disabled="isSaving" class="mt-4">
@@ -162,6 +181,8 @@ let logoFileName = ref(null)
const companyForm = reactive({
name: null,
tax_id: null,
vat_id: null,
address: {
address_street_1: '',
address_street_2: '',
@@ -200,13 +221,13 @@ const rules = {
address_street_1: {
maxLength: helpers.withMessage(
t('validation.address_maxlength', { count: 255 }),
maxLength(255)
maxLength(255),
),
},
address_street_2: {
maxLength: helpers.withMessage(
t('validation.address_maxlength', { count: 255 }),
maxLength(255)
maxLength(255),
),
},
},
@@ -241,7 +262,7 @@ async function next() {
JSON.stringify({
name: logoFileName.value,
data: logoFileBlob.value,
})
}),
)
await companyStore.updateCompanyLogo(logoData)
}

View File

@@ -147,6 +147,7 @@ import {
decimal,
} from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { cloneDeep } from 'lodash'
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
import { useModuleStore } from '@/scripts/admin/stores/module'
@@ -258,11 +259,29 @@ async function submitForm() {
isSaving.value = true
let data = {
let data = cloneDeep({
...invoiceStore.newInvoice,
sub_total: invoiceStore.getSubTotal,
total: invoiceStore.getTotal,
tax: invoiceStore.getTotalTax,
})
if (data.discount_per_item === 'YES') {
data.items.forEach((item, index) => {
if (item.discount_type === 'fixed'){
data.items[index].discount = item.discount * 100
}
})
}
else {
if (data.discount_type === 'fixed'){
data.discount = data.discount * 100
}
}
if (
!invoiceStore.newInvoice.tax_per_item === 'YES'
&& data.taxes.length
){
data.tax_type_ids = data.taxes.map(_t => _t.tax_type_id)
}
try {

View File

@@ -76,6 +76,7 @@
label="name"
value-prop="code"
track-by="name"
:searchable="true"
open-direction="top"
/>
</BaseInputGroup>

View File

@@ -84,6 +84,16 @@
class="mt-2"
/>
</div>
<div class="space-y-6">
<BaseInputGroup :label="$t('settings.company_info.tax_id')">
<BaseInput v-model="companyForm.tax_id" type="text" />
</BaseInputGroup>
<BaseInputGroup :label="$t('settings.company_info.vat_id')">
<BaseInput v-model="companyForm.vat_id" type="text" />
</BaseInputGroup>
</div>
</BaseInputGrid>
<BaseButton
@@ -111,24 +121,7 @@
<div class="mt-5">
<button
type="button"
class="
inline-flex
items-center
justify-center
px-4
py-2
border border-transparent
font-medium
rounded-md
text-red-700
bg-red-100
hover:bg-red-200
focus:outline-none
focus:ring-2
focus:ring-offset-2
focus:ring-red-500
sm:text-sm
"
class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
@click="removeCompany"
>
{{ $t('general.delete') }}
@@ -161,6 +154,8 @@ let isSaving = ref(false)
const companyForm = reactive({
name: null,
logo: null,
tax_id: null,
vat_id: null,
address: {
address_street_1: '',
address_street_2: '',
@@ -194,7 +189,7 @@ const rules = computed(() => {
required: helpers.withMessage(t('validation.required'), required),
minLength: helpers.withMessage(
t('validation.name_min_length'),
minLength(3)
minLength(3),
),
},
address: {
@@ -207,7 +202,7 @@ const rules = computed(() => {
const v$ = useVuelidate(
rules,
computed(() => companyForm)
computed(() => companyForm),
)
globalStore.fetchCountries()
@@ -243,7 +238,7 @@ async function updateCompanyData() {
JSON.stringify({
name: logoFileName.value,
data: logoFileBlob.value,
})
}),
)
}
logoData.append('is_company_logo_removed', isCompanyLogoRemoved.value)

View File

@@ -93,7 +93,7 @@ import FileDiskModal from '@/scripts/admin/components/modal-components/FileDiskM
const utils = inject('utils')
const modelStore = useModalStore()
const modalStore = useModalStore()
const diskStore = useDiskStore()
const companyStore = useCompanyStore()
const dialogStore = useDialogStore()
@@ -189,7 +189,7 @@ function isNotSystemDisk(disk) {
}
function openCreateDiskModal() {
modelStore.openModal({
modalStore.openModal({
title: t('settings.disk.new_disk'),
componentName: 'FileDiskModal',
variant: 'lg',
@@ -198,7 +198,7 @@ function openCreateDiskModal() {
}
function openEditDiskModal(data) {
modelStore.openModal({
modalStore.openModal({
title: t('settings.disk.edit_file_disk'),
componentName: 'FileDiskModal',
variant: 'lg',

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