Merge pull request #8263 from turbo124/v5-stable

v5.5.69
This commit is contained in:
David Bomba 2023-02-09 00:28:49 +11:00 committed by GitHub
commit bf6526683f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 219122 additions and 217916 deletions

View file

@ -10,9 +10,7 @@
## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org)
Join us on [Slack](http://slack.invoiceninja.com), [Discord](https://discord.com/channels/1071654583870435439/1071654584390537279) [Discourse](https://forum.invoiceninja.com) -
or [StackOverflow](https://stackoverflow.com/tags/invoice-ninja/) if you like,
just make sure to add the `invoice-ninja` tag to your question.
Join us on [Slack](http://slack.invoiceninja.com), [Discord](https://discord.gg/ZwEdtfCwXA), [Support Forum](https://forum.invoiceninja.com)
## Introduction
@ -27,13 +25,13 @@ We offer a $30 per year white-label license to remove the Invoice Ninja branding
* [API Documentation](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja)
* [APP Documentation](https://invoiceninja.github.io/)
* [Support Forum](https://forum.invoiceninja.com)
* [StackOverflow](https://stackoverflow.com/tags/invoice-ninja/)
## Setup
### Mobile Apps
* [iPhone](https://apps.apple.com/app/id1503970375?platform=iphone)
* [Android](https://play.google.com/store/apps/details?id=com.invoiceninja.app)
* [F-Droid](https://f-droid.org/en/packages/com.invoiceninja.app)
### Desktop Apps
* [macOS](https://apps.apple.com/app/id1503970375?platform=mac)
@ -55,7 +53,7 @@ We offer a $30 per year white-label license to remove the Invoice Ninja branding
git clone https://github.com/invoiceninja/invoiceninja.git
git checkout v5-stable
cp .env.example .env
composer update
composer i -o --no-dev
php artisan key:generate
```

View file

@ -1 +1 @@
5.5.68
5.5.69

View file

@ -834,7 +834,6 @@ class CompanySettings extends BaseSettings
'$client.address1',
'$client.address2',
'$client.city_state_postal',
'$client.postal_city',
'$client.country',
'$client.phone',
'$contact.email',
@ -846,7 +845,6 @@ class CompanySettings extends BaseSettings
'$vendor.address1',
'$vendor.address2',
'$vendor.city_state_postal',
'$vendor.postal_city',
'$vendor.country',
'$vendor.phone',
'$contact.email',
@ -871,7 +869,6 @@ class CompanySettings extends BaseSettings
'$company.address1',
'$company.address2',
'$company.city_state_postal',
'$company.postal_city',
'$company.country',
],
'invoice_details' => [

View file

@ -1031,6 +1031,11 @@ class BaseController extends Controller
public function flutterRoute()
{
if ((bool) $this->checkAppSetup() !== false && $account = Account::first()) {
//always redirect invoicing.co to invoicing.co
if(Ninja::isHosted() && (request()->getSchemeAndHttpHost() != 'https://invoicing.co'))
return redirect()->secure('https://invoicing.co');
if (config('ninja.require_https') && ! request()->isSecure()) {
return redirect()->secure(request()->getRequestUri());
}

View file

@ -322,6 +322,7 @@ class BillingPortalPurchasev2 extends Component
'total' => $total,
'qty' => $qty,
'is_recurring' => true,
'product_image' => $p->product_image,
]);
}

View file

@ -42,7 +42,7 @@ class GenericReportRequest extends Request
{
$input = $this->all();
if (! array_key_exists('date_range', $input)) {
if (! array_key_exists('date_range', $input) || $input['date_range'] == '') {
$input['date_range'] = 'all';
}

View file

@ -46,7 +46,7 @@ class ProductSalesReportRequest extends Request
{
$input = $this->all();
if (! array_key_exists('date_range', $input)) {
if (! array_key_exists('date_range', $input) || $input['date_range'] == '') {
$input['date_range'] = 'all';
}

View file

@ -42,7 +42,7 @@ class ProfitLossRequest extends Request
{
$input = $this->all();
if (! array_key_exists('date_range', $input)) {
if (! array_key_exists('date_range', $input) || $input['date_range'] == '') {
$input['date_range'] = 'all';
}

View file

@ -195,4 +195,56 @@ class Request extends FormRequest
public function prepareForValidation()
{
}
public function checkTimeLog(array $log): bool
{
if(count($log) == 0)
return true;
/*Get first value of all arrays*/
$result = array_column($log, 0);
/*Sort the array in ascending order*/
asort($result);
$new_array = [];
/*Rebuild the array in order*/
foreach($result as $key => $value)
$new_array[] = $log[$key];
/*Iterate through the array and perform checks*/
foreach($new_array as $key => $array)
{
/*Flag which helps us know if there is a NEXT timelog*/
$next = false;
/* If there are more than 1 time log in the array, ensure the last timestamp is not zero*/
if(count($new_array) >1 && $array[1] == 0)
return false;
/* Check if the start time is greater than the end time */
/* Ignore the last value for now, we'll do a separate check for this */
if($array[0] > $array[1] && $array[1] != 0)
return false;
/* Find the next time log value - if it exists */
if(array_key_exists($key+1, $new_array))
$next = $new_array[$key+1];
/* check the next time log and ensure the start time is GREATER than the end time of the previous record */
if($next && $next[0] < $array[1])
return false;
/* Get the last row of the timelog*/
$last_row = end($new_array);
/*If the last value is NOT zero, ensure start time is not GREATER than the endtime */
if($last_row[1] != 0 && $last_row[0] > $last_row[1])
return false;
return true;
}
}
}

View file

@ -53,6 +53,9 @@ class StoreTaskRequest extends Request
$fail('The '.$attribute.' - '.print_r($k,1).' is invalid. Unix timestamps only.');
}
if(!$this->checkTimeLog($values))
$fail('Please correct overlapping values');
}];

View file

@ -59,6 +59,9 @@ class UpdateTaskRequest extends Request
$fail('The '.$attribute.' - '.print_r($k,1).' is invalid. Unix timestamps only.');
}
if(!$this->checkTimeLog($values))
$fail('Please correct overlapping values');
}];
return $this->globalRules($rules);

View file

@ -39,6 +39,8 @@ class StoreWebhookRequest extends Request
{
$input = $this->all();
if(!isset($input['rest_method']))
$input['rest_method'] = 'post';
// if(isset($input['headers']) && count($input['headers']) == 0)
// $input['headers'] = null;

View file

@ -44,6 +44,9 @@ class UpdateWebhookRequest extends Request
{
$input = $this->all();
if(!isset($input['rest_method']))
$input['rest_method'] = 'post';
// if(isset($input['headers']) && count($input['headers']) == 0)
// $input['headers'] = null;

View file

@ -138,6 +138,17 @@ class InvoiceEmailEngine extends BaseEmailEngine
if ($this->client->getSetting('document_email_attachment') !== false && $this->invoice->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) {
if($this->invoice->recurring_invoice()->exists())
{
foreach ($this->invoice->recurring_invoice->documents as $document) {
if($document->size > $this->max_attachment_size)
$this->setAttachmentLinks(["<a class='doc_links' href='" . URL::signedRoute('documents.public_download', ['document_hash' => $document->hash]) ."'>". $document->name ."</a>"]);
else
$this->setAttachments([['file' => base64_encode($document->getFile()), 'path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL, ]]);
}
}
// Storage::url
foreach ($this->invoice->documents as $document) {

View file

@ -18,7 +18,6 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class BankTransactionRule extends BaseModel
{
use SoftDeletes;
use MakesHash;
use Filterable;
protected $fillable = [
@ -66,6 +65,37 @@ class BankTransactionRule extends BaseModel
private array $search_results = [];
public function getEntityType()
{
return self::class;
}
public function company()
{
return $this->belongsTo(Company::class);
}
public function vendor()
{
return $this->belongsTo(Vendor::class);
}
public function client()
{
return $this->belongsTo(Client::class);
}
public function user()
{
return $this->belongsTo(User::class)->withTrashed();
}
public function expense_category()
{
return $this->belongsTo(ExpenseCategory::class, 'category_id')->withTrashed();
}
// rule object looks like this:
//[
// {
@ -138,34 +168,5 @@ class BankTransactionRule extends BaseModel
// }
// }
public function getEntityType()
{
return self::class;
}
public function company()
{
return $this->belongsTo(Company::class);
}
public function vendor()
{
return $this->belongsTo(Vendor::class);
}
public function client()
{
return $this->belongsTo(Client::class);
}
public function user()
{
return $this->belongsTo(User::class)->withTrashed();
}
public function expense_cateogry()
{
return $this->belongsTo(ExpenseCategory::class)->withTrashed();
}
}

View file

@ -12,6 +12,7 @@
namespace App\Models;
use App\DataMapper\ClientSettings;
use App\Jobs\Util\WebhookHandler;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\UserSessionAttributes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -167,7 +168,7 @@ class BaseModel extends Model
*/
public function resolveRouteBinding($value, $field = null)
{
if (is_numeric($value)) {
throw new ModelNotFoundException("Record with value {$value} not found");
}
@ -189,12 +190,12 @@ class BaseModel extends Model
public function numberFormatter()
{
$number = strlen($this->number) >= 1 ? $this->translate_entity() . "_" . $this->number : class_basename($this) . "_" . Str::random(5);
$number = strlen($this->number) >= 1 ? $this->translate_entity() . "_" . $this->number : class_basename($this) . "_" . Str::random(5);
$formatted_number = mb_ereg_replace("([^\w\s\d\-_~,;\[\]\(\).])", '', $number);
$formatted_number = mb_ereg_replace("([\.]{2,})", '', $formatted_number);
$formatted_number = preg_replace('/\s+/', '_', $formatted_number);
return $formatted_number;
@ -205,4 +206,24 @@ class BaseModel extends Model
return ctrans('texts.item');
}
/**
* Model helper to send events for webhooks
*
* @param int $event_id
* @param string $additional_data optional includes
*
* @return void
*/
public function sendEvent(int $event_id, string $additional_data = ""): void
{
$subscriptions = Webhook::where('company_id', $this->company_id)
->where('event_id', $event_id)
->exists();
if ($subscriptions) {
WebhookHandler::dispatch($event_id, $this, $this->company, $additional_data);
}
}
}

View file

@ -39,6 +39,8 @@ class Product extends BaseModel
'in_stock_quantity',
'stock_notification_threshold',
'stock_notification',
'max_quantity',
'product_image',
];
protected $touches = [];

View file

@ -136,6 +136,14 @@ class Webhook extends BaseModel
const EVENT_ARCHIVE_PURCHASE_ORDER = 59; //tested
const EVENT_SENT_INVOICE = 60;
const EVENT_SENT_QUOTE = 61;
const EVENT_SENT_CREDIT = 62;
const EVENT_SENT_PURCHASE_ORDER = 63;
public static $valid_events = [
self::EVENT_CREATE_PURCHASE_ORDER,
self::EVENT_UPDATE_PURCHASE_ORDER,
@ -194,7 +202,11 @@ class Webhook extends BaseModel
self::EVENT_RESTORE_QUOTE,
self::EVENT_RESTORE_INVOICE,
self::EVENT_RESTORE_PAYMENT,
self::EVENT_RESTORE_VENDOR
self::EVENT_RESTORE_VENDOR,
self::EVENT_SENT_INVOICE,
self::EVENT_SENT_QUOTE,
self::EVENT_SENT_CREDIT,
self::EVENT_SENT_PURCHASE_ORDER
];

View file

@ -82,7 +82,7 @@ class ClientContactObserver
CreditInvitation::withTrashed()->where('client_contact_id', $client_contact_id)->cursor()->each(function ($invite){
if($invite->credits()->doesnthave('invitations'))
if($invite->credit()->doesnthave('invitations'))
$invite->credit->service()->createInvitations();
});

View file

@ -59,7 +59,7 @@ class ClientObserver
->exists();
if ($subscriptions)
WebhookHandler::dispatch($event, $client, $client->company)->delay(0);
WebhookHandler::dispatch($event, $client, $client->company, 'client')->delay(0);
}

View file

@ -58,7 +58,7 @@ class InvoiceObserver
->exists();
if ($subscriptions)
WebhookHandler::dispatch($event, $invoice, $invoice->company)->delay(0);
WebhookHandler::dispatch($event, $invoice, $invoice->company, 'client')->delay(0);
}
/**

View file

@ -43,6 +43,11 @@ class CustomPaymentDriver extends BaseDriver
return $types;
}
public function init()
{
return $this;
}
public function setPaymentMethod($payment_method_id)
{
$this->payment_method = $payment_method_id;
@ -101,4 +106,9 @@ class CustomPaymentDriver extends BaseDriver
{
// Driver doesn't support this feature.
}
public function getClientRequiredFields(): array
{
return [];
}
}

View file

@ -12,7 +12,9 @@
namespace App\Services\Credit;
use App\Events\Credit\CreditWasMarkedSent;
use App\Jobs\Util\WebhookHandler;
use App\Models\Credit;
use App\Models\Webhook;
use App\Utils\Ninja;
class MarkSent
@ -49,9 +51,11 @@ class MarkSent
->service()
->adjustCreditBalance($this->credit->amount)
->save();
event(new CreditWasMarkedSent($this->credit, $this->credit->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
$this->credit->sendEvent(Webhook::EVENT_SENT_CREDIT);
return $this->credit;
}
}

View file

@ -36,6 +36,7 @@ class ApplyNumber extends AbstractService
public function run()
{
if ($this->invoice->number != '') {
return $this->invoice;
}
@ -45,7 +46,7 @@ class ApplyNumber extends AbstractService
$this->trySaving();
break;
case 'when_sent':
if ($this->invoice->status_id == Invoice::STATUS_SENT) {
if ($this->invoice->status_id >= Invoice::STATUS_SENT) {
$this->trySaving();
}
break;

View file

@ -12,8 +12,10 @@
namespace App\Services\Invoice;
use App\Events\Invoice\InvoiceWasUpdated;
use App\Jobs\Util\WebhookHandler;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\Webhook;
use App\Services\AbstractService;
use App\Utils\Ninja;
@ -71,6 +73,7 @@ class MarkSent extends AbstractService
if($fire_webhook)
event('eloquent.updated: App\Models\Invoice', $this->invoice);
$this->invoice->sendEvent(Webhook::EVENT_SENT_INVOICE, "client");
return $this->invoice->fresh();
}

View file

@ -85,7 +85,11 @@ class UpdateInvoicePayment
if($invoice->is_proforma)
{
$invoice->number = '';
if(strlen($invoice->number) > 1 && str_starts_with($invoice->number,"####"))
$invoice->number = '';
$invoice->is_proforma = false;
$invoice->service()

View file

@ -11,7 +11,9 @@
namespace App\Services\PurchaseOrder;
use App\Jobs\Util\WebhookHandler;
use App\Models\PurchaseOrder;
use App\Models\Webhook;
use App\Utils\Ninja;
class MarkSent
@ -43,6 +45,8 @@ class MarkSent
->adjustBalance($this->purchase_order->amount) //why was this commented out previously?
->save();
$this->purchase_order->sendEvent(Webhook::EVENT_SENT_PURCHASE_ORDER, "vendor");
return $this->purchase_order;
}
}

View file

@ -12,7 +12,9 @@
namespace App\Services\Quote;
use App\Events\Quote\QuoteWasMarkedSent;
use App\Jobs\Util\WebhookHandler;
use App\Models\Quote;
use App\Models\Webhook;
use App\Utils\Ninja;
use Carbon\Carbon;
@ -52,6 +54,8 @@ class MarkSent
event(new QuoteWasMarkedSent($this->quote, $this->quote->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
$this->quote->sendEvent(Webhook::EVENT_SENT_QUOTE, "client");
return $this->quote;
}
}

View file

@ -958,7 +958,7 @@ class SubscriptionService
$invoice->subscription_id = $this->subscription->id;
$invoice->client_id = $client_id;
$invoice->is_proforma = true;
$invoice->number = ctrans('texts.subscription') . "_" . now()->format('Y-m-d') . "_" . rand(0,100000);
$invoice->number = "####" . ctrans('texts.subscription') . "_" . now()->format('Y-m-d') . "_" . rand(0,100000);
$line_items = $bundle->map(function ($item){
$line_item = new InvoiceItem;

View file

@ -11,12 +11,14 @@
namespace App\Transformers;
use App\Models\Account;
use App\Models\BankTransaction;
use App\Models\BankTransactionRule;
use App\Models\Client;
use App\Models\Company;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\ExpenseCategory;
use App\Models\Vendor;
use App\Transformers\ExpenseCategoryTransformer;
use App\Transformers\ExpenseCateogryTransformer;
use App\Transformers\VendorTransformer;
use App\Utils\Traits\MakesHash;
@ -75,16 +77,35 @@ class BankTransactionRuleTransformer extends EntityTransformer
public function includeClient(BankTransactionRule $bank_transaction_rule)
{
$transformer = new ClientTransformer($this->serializer);
return $this->includeItem($bank_transaction_rule->expense, $transformer, Client::class);
if(!$bank_transaction_rule->client)
return null;
$transformer = new ClientTransformer($this->serializer);
return $this->includeItem($bank_transaction_rule->client, $transformer, Client::class);
}
public function includeVendor(BankTransactionRule $bank_transaction_rule)
{
if(!$bank_transaction_rule->vendor)
return null;
$transformer = new VendorTransformer($this->serializer);
return $this->includeItem($bank_transaction_rule->vendor, $transformer, Vendor::class);
}
public function includeExpenseCategory(BankTransactionRule $bank_transaction_rule)
{
if(!$bank_transaction_rule->expense_category)
return null;
$transformer = new ExpenseCategoryTransformer($this->serializer);
return $this->includeItem($bank_transaction_rule->expense_category, $transformer, ExpenseCategory::class);
}
}

View file

@ -93,6 +93,8 @@ class ProductTransformer extends EntityTransformer
'in_stock_quantity' => (int) $product->in_stock_quantity ?: 0,
'stock_notification' => (bool) $product->stock_notification,
'stock_notification_threshold' => (int) $product->stock_notification_threshold,
'max_quantity' => (int) $product->max_quantity,
'product_image' => (string) $product->product_image ?: '',
];
}
}

View file

@ -223,15 +223,15 @@ class Number
/* 08-01-2022 allow increased precision for unit price*/
$v = rtrim(sprintf('%f', $value), '0');
// $precision = strlen(substr(strrchr($v, $decimal), 1));
if ($v < 1) {
/* 08-02-2023 special if block to render $0.5 to $0.50*/
if ($v < 1 && strlen($v) == 3) {
$precision = 2;
}
elseif ($v < 1) {
$precision = strlen($v) - strrpos($v, '.') - 1;
}
// if($precision == 1)
// $precision = 2;
$value = number_format($v, $precision, $decimal, $thousand);
$symbol = $currency->symbol;

View file

@ -74,7 +74,7 @@
"omnipay/paypal": "^3.0",
"payfast/payfast-php-sdk": "^1.1",
"pragmarx/google2fa": "^8.0",
"turbo124/predis": "^1.1",
"predis/predis": "2.*",
"razorpay/razorpay": "2.*",
"sentry/sentry-laravel": "^3",
"setasign/fpdf": "^1.8",

690
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.5.68',
'app_tag' => '5.5.68',
'app_version' => '5.5.69',
'app_tag' => '5.5.69',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View file

@ -0,0 +1,48 @@
<?php
use App\Models\Company;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('products', function (Blueprint $table){
$table->unsignedInteger("max_quantity")->nullable();
$table->string("product_image", 191)->nullable();
});
Company::query()
->chunk(1000, function ($companies) {
foreach($companies as $c)
{
$settings = $c->settings;
$settings->font_size = 16;
$c->settings = $settings;
$c->save();
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
};

View file

@ -4,8 +4,8 @@ const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"flutter.js": "f85e6fb278b0fd20c349186fb46ae36d",
"/": "6a3595d7c5906d433b5fb9a8a70c928d",
"main.dart.js": "18a61fecfbb7fe6dab328ee81428b7da",
"/": "0a440124fb4146691e44c002bbf24bb8",
"main.dart.js": "811f1fb072771983db12e1a6f2c3b89d",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"favicon.png": "dca91c54388f52eded692718d5a98b8b",

View file

@ -1,2 +1,2 @@
/*! For license information please see approve.js.LICENSE.txt */
(()=>{function e(e,t){for(var n=0;n<t.length;n++){var u=t[n];u.enumerable=u.enumerable||!1,u.configurable=!0,"value"in u&&(u.writable=!0),Object.defineProperty(e,u.key,u)}}var t=function(){function t(e,n,u){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t),this.shouldDisplaySignature=e,this.shouldDisplayTerms=n,this.shouldDisplayUserInput=u,this.termsAccepted=!1}var n,u,a;return n=t,(u=[{key:"submitForm",value:function(){document.getElementById("approve-form").submit()}},{key:"displaySignature",value:function(){document.getElementById("displaySignatureModal").removeAttribute("style");var e=new SignaturePad(document.getElementById("signature-pad"),{penColor:"rgb(0, 0, 0)"});e.onEnd=function(){document.getElementById("signature-next-step").disabled=!1},this.signaturePad=e}},{key:"displayTerms",value:function(){document.getElementById("displayTermsModal").removeAttribute("style")}},{key:"displayInput",value:function(){document.getElementById("displayInputModal").removeAttribute("style")}},{key:"handle",value:function(){var e=this;document.getElementById("signature-next-step").disabled=!0,document.getElementById("close_button").addEventListener("click",(function(){var e=document.getElementById("approve-button");e&&(e.disabled=!1)})),document.getElementById("hide_close").addEventListener("click",(function(){var e=document.getElementById("approve-button");e&&(e.disabled=!1)})),document.getElementById("approve-button").addEventListener("click",(function(){e.shouldDisplaySignature||e.shouldDisplayTerms||!e.shouldDisplayUserInput||(e.displayInput(),document.getElementById("input-next-step").addEventListener("click",(function(){document.querySelector('input[name="user_input"').value=document.getElementById("user_input").value,e.termsAccepted=!0,e.submitForm()}))),e.shouldDisplayUserInput&&e.displayInput(),e.shouldDisplaySignature&&e.shouldDisplayTerms&&(e.displaySignature(),document.getElementById("signature-next-step").addEventListener("click",(function(){e.displayTerms(),document.getElementById("accept-terms-button").addEventListener("click",(function(){document.querySelector('input[name="signature"').value=e.signaturePad.toDataURL(),document.querySelector('input[name="user_input"').value=document.getElementById("user_input").value,e.termsAccepted=!0,e.submitForm()}))}))),e.shouldDisplaySignature&&!e.shouldDisplayTerms&&(e.displaySignature(),document.getElementById("signature-next-step").addEventListener("click",(function(){document.querySelector('input[name="signature"').value=e.signaturePad.toDataURL(),document.querySelector('input[name="user_input"').value=document.getElementById("user_input").value,e.submitForm()}))),!e.shouldDisplaySignature&&e.shouldDisplayTerms&&(e.displayTerms(),document.getElementById("accept-terms-button").addEventListener("click",(function(){e.termsAccepted=!0,e.submitForm()}))),e.shouldDisplaySignature||e.shouldDisplayTerms||!e.shouldDisplayUserInput||e.submitForm()}))}}])&&e(n.prototype,u),a&&e(n,a),Object.defineProperty(n,"prototype",{writable:!1}),t}(),n=document.querySelector('meta[name="require-quote-signature"]').content,u=document.querySelector('meta[name="show-quote-terms"]').content,a=document.querySelector('meta[name="accept-user-input"]').content;new t(Boolean(+n),Boolean(+u),Boolean(+a)).handle()})();
(()=>{function e(e,t){for(var n=0;n<t.length;n++){var u=t[n];u.enumerable=u.enumerable||!1,u.configurable=!0,"value"in u&&(u.writable=!0),Object.defineProperty(e,u.key,u)}}var t=function(){function t(e,n,u){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t),this.shouldDisplaySignature=e,this.shouldDisplayTerms=n,this.shouldDisplayUserInput=u,this.termsAccepted=!1}var n,u,a;return n=t,(u=[{key:"submitForm",value:function(){document.getElementById("approve-form").submit()}},{key:"displaySignature",value:function(){document.getElementById("displaySignatureModal").removeAttribute("style");var e=new SignaturePad(document.getElementById("signature-pad"),{penColor:"rgb(0, 0, 0)"});e.onEnd=function(){document.getElementById("signature-next-step").disabled=!1},this.signaturePad=e}},{key:"displayTerms",value:function(){document.getElementById("displayTermsModal").removeAttribute("style")}},{key:"displayInput",value:function(){document.getElementById("displayInputModal").removeAttribute("style")}},{key:"handle",value:function(){var e=this;document.getElementById("signature-next-step").disabled=!0,document.getElementById("close_button").addEventListener("click",(function(){var e=document.getElementById("approve-button");e&&(e.disabled=!1)})),document.getElementById("hide_close").addEventListener("click",(function(){var e=document.getElementById("approve-button");e&&(e.disabled=!1)})),document.getElementById("approve-button").addEventListener("click",(function(){e.shouldDisplaySignature||e.shouldDisplayTerms||!e.shouldDisplayUserInput||(e.displayInput(),document.getElementById("input-next-step").addEventListener("click",(function(){document.querySelector('input[name="user_input"').value=document.getElementById("user_input").value,e.termsAccepted=!0,e.submitForm()}))),e.shouldDisplayUserInput&&e.displayInput(),e.shouldDisplaySignature&&e.shouldDisplayTerms&&(e.displaySignature(),document.getElementById("signature-next-step").addEventListener("click",(function(){e.displayTerms(),document.getElementById("accept-terms-button").addEventListener("click",(function(){document.querySelector('input[name="signature"').value=e.signaturePad.toDataURL(),document.querySelector('input[name="user_input"').value=document.getElementById("user_input").value,e.termsAccepted=!0,e.submitForm()}))}))),e.shouldDisplaySignature&&!e.shouldDisplayTerms&&(e.displaySignature(),document.getElementById("signature-next-step").addEventListener("click",(function(){document.querySelector('input[name="signature"').value=e.signaturePad.toDataURL(),document.querySelector('input[name="user_input"').value=document.getElementById("user_input").value,e.submitForm()}))),!e.shouldDisplaySignature&&e.shouldDisplayTerms&&(e.displayTerms(),document.getElementById("accept-terms-button").addEventListener("click",(function(){e.termsAccepted=!0,e.submitForm()}))),e.shouldDisplaySignature||e.shouldDisplayTerms||e.shouldDisplayUserInput||e.submitForm()}))}}])&&e(n.prototype,u),a&&e(n,a),Object.defineProperty(n,"prototype",{writable:!1}),t}(),n=document.querySelector('meta[name="require-quote-signature"]').content,u=document.querySelector('meta[name="show-quote-terms"]').content,a=document.querySelector('meta[name="accept-user-input"]').content;new t(Boolean(+n),Boolean(+u),Boolean(+a)).handle()})();

212318
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

206422
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,7 @@
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=0274ab4f8d2b411f2a2fe5142301e7af",
"/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js?id=4bd34a0b160f6f29b3096d870ac4d308",
"/js/clients/quotes/action-selectors.js": "/js/clients/quotes/action-selectors.js?id=6fb63bae43d077b5061f4dadfe8dffc8",
"/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js?id=61a346e1977d3a1fec3634b234baa25c",
"/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js?id=2cb18f2df99d0eca47fa34f1d652c34f",
"/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=809de47258a681f0ffebe787dd6a9a93",
"/js/setup/setup.js": "/js/setup/setup.js?id=27560b012f166f8b9417ced2188aab70",
"/js/clients/payments/card-js.min.js": "/js/clients/payments/card-js.min.js?id=8ce33c3deae058ad314fb8357e5be63b",

View file

@ -75,7 +75,7 @@ class Approve {
.getElementById('approve-button')
.addEventListener('click', () => {
if (!this.shouldDisplaySignature && !this.shouldDisplayTerms && this.shouldDisplayUserInput){
if (!this.shouldDisplaySignature && !this.shouldDisplayTerms && this.shouldDisplayUserInput){
this.displayInput();
document
@ -146,7 +146,7 @@ class Approve {
});
}
if (!this.shouldDisplaySignature && !this.shouldDisplayTerms && this.shouldDisplayUserInput) {
if (!this.shouldDisplaySignature && !this.shouldDisplayTerms && !this.shouldDisplayUserInput) {
this.submitForm();
}
});

View file

@ -11,10 +11,15 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: $font_name, Helvetica, sans-serif;
font-size: "$font_size";
font-size: $font_size !important;
zoom: 80%;
}
table tr td, table tr, th {
font-size: $font_size !important;
}
body, html {
margin: 0;
padding: 0;
@ -114,7 +119,7 @@
min-width: 100%;
table-layout: fixed;
overflow-wrap: break-word;
margin-top: 3rem;
margin-top: 0rem;
margin-bottom: 0px;
}

View file

@ -16,10 +16,16 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: $font_name, Helvetica, sans-serif;
font-size: "$font_size";
font-size: $font_size !important;
zoom: 80%;
}
table tr td, table tr, th {
font-size: $font_size !important;
}
@page {
margin-left: $global_margin;
margin-right: $global_margin;

View file

@ -11,10 +11,15 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: $font_name, Helvetica, sans-serif;
font-size: "$font_size";
font-size: $font_size !important;
zoom: 80%;
}
table tr td, table tr, th {
font-size: $font_size !important;
}
html {
margin: 0;
padding-top: 1rem;

View file

@ -16,10 +16,14 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: $font_name, Helvetica, sans-serif;
font-size: "$font_size";
font-size: $font_size !important;
zoom: 80%;
}
table tr td, table tr, th {
font-size: $font_size !important;
}
@page {
margin-left: $global_margin;
margin-right: $global_margin;

View file

@ -16,10 +16,15 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: $font_name, Helvetica, sans-serif;
font-size: "$font_size";
font-size: $font_size !important;
zoom: 80%;
}
table tr td, table tr, th {
font-size: $font_size !important;
}
@page {
margin-left: $global_margin;
margin-right: $global_margin;

View file

@ -16,10 +16,14 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: $font_name, Helvetica, sans-serif;
font-size: "$font_size";
font-size: $font_size !important;
zoom: 80%;
}
table tr td, table tr, th {
font-size: $font_size !important;
}
@page {
margin: $global_margin;
size: $page_size $page_layout;

View file

@ -16,10 +16,14 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: $font_name, Helvetica, sans-serif;
font-size: "$font_size";
font-size: $font_size !important;
zoom: 80%;
}
table tr td, table tr, th {
font-size: $font_size !important;
}
@page {
margin: $global_margin;
size: $page_size $page_layout;

View file

@ -15,10 +15,14 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: $font_name, Helvetica, sans-serif;
font-size: "7px";
font-size: $font_size !important;
zoom: 80%;
}
table tr td, table tr, th {
font-size: $font_size !important;
}
body, html {
margin: 0;
padding: 0;

View file

@ -11,10 +11,14 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: $font_name, Helvetica, sans-serif;
font-size: "$font_size";
font-size: $font_size !important;
zoom: 80%;
}
table tr td, table tr, th {
font-size: $font_size !important;
}
html {
width: 210mm;
height: 200mm;

View file

@ -11,10 +11,14 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: $font_name, Helvetica, sans-serif;
font-size: "$font_size";
font-size: $font_size !important;
zoom: 80%;
}
table tr td, table tr, th {
font-size: $font_size !important;
}
html {
width: 210mm;
height: 200mm;

View file

@ -15,11 +15,15 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: $font_name, Helvetica, sans-serif;
font-size: "$font_size";
font-size: $font_size !important;
zoom: 80%;
margin: 0;
}
table tr td, table tr, th {
font-size: $font_size !important;
}
@page {
margin: 0;
size: $page_size $page_layout;

View file

@ -36,9 +36,9 @@
@if(!empty($subscription->recurring_product_ids))
@foreach($recurring_products as $index => $product)
<li class="flex py-6">
@if(filter_var($product->custom_value1, FILTER_VALIDATE_URL))
@if(filter_var($product->product_image, FILTER_VALIDATE_URL))
<div class="h-24 w-24 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 mr-2">
<img src="{{$product->custom_value1}}" alt="" class="h-full w-full object-cover object-center p-2">
<img src="{{$product->product_image}}" alt="" class="h-full w-full object-cover object-center p-2">
</div>
@endif
<div class="ml-0 flex flex-1 flex-col">
@ -74,7 +74,7 @@
@endfor
}
@else
@for ($i = 2; $i <= ($subscription->use_inventory_management ? min($product->in_stock_quantity, max(100,$product->custom_value2)) : max(100,$product->custom_value2)); $i++)
@for ($i = 2; $i <= ($subscription->use_inventory_management ? min($product->in_stock_quantity, max(100,$product->max_quantity)) : max(100,$product->max_quantity)); $i++)
<option value="{{$i}}">{{$i}}</option>
@endfor
@endif
@ -96,9 +96,9 @@
@if(!empty($subscription->product_ids))
@foreach($products as $product)
<li class="flex py-6">
@if(filter_var($product->custom_value1, FILTER_VALIDATE_URL))
@if(filter_var($product->product_image, FILTER_VALIDATE_URL))
<div class="h-24 w-24 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 mr-2">
<img src="{{$product->custom_value1}}" alt="" class="h-full w-full object-cover object-center p-2">
<img src="{{$product->product_image}}" alt="" class="h-full w-full object-cover object-center p-2">
</div>
@endif
<div class="ml-0 flex flex-1 flex-col">
@ -135,9 +135,9 @@
@if(!empty($subscription->optional_recurring_product_ids))
@foreach($optional_recurring_products as $index => $product)
<li class="flex py-6">
@if(filter_var($product->custom_value1, FILTER_VALIDATE_URL))
@if(filter_var($product->product_image, FILTER_VALIDATE_URL))
<div class="h-24 w-24 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 mr-2">
<img src="{{$product->custom_value1}}" alt="" class="h-full w-full object-cover object-center p-2">
<img src="{{$product->product_image}}" alt="" class="h-full w-full object-cover object-center p-2">
</div>
@endif
<div class="ml-0 flex flex-1 flex-col">
@ -148,7 +148,7 @@
</div>
</div>
<div class="flex justify-between text-sm mt-1">
@if(is_numeric($product->custom_value2))
@if(is_numeric($product->max_quantity))
<p class="text-gray-500 w-3/4"></p>
<div class="flex place-content-end">
@if($subscription->use_inventory_management && $product->in_stock_quantity == 0)
@ -162,7 +162,7 @@
@endif
>
<option value="0" selected="selected">0</option>
@for ($i = 1; $i <= ($subscription->use_inventory_management ? min($product->in_stock_quantity, max(100,$product->custom_value2)) : max(100,$product->custom_value2)); $i++)
@for ($i = 1; $i <= ($subscription->use_inventory_management ? min($product->in_stock_quantity, max(100,$product->max_quantity)) : max(100,$product->max_quantity)); $i++)
<option value="{{$i}}">{{$i}}</option>
@endfor
</select>
@ -176,9 +176,9 @@
@if(!empty($subscription->optional_product_ids))
@foreach($optional_products as $index => $product)
<li class="flex py-6">
@if(filter_var($product->custom_value1, FILTER_VALIDATE_URL))
@if(filter_var($product->product_image, FILTER_VALIDATE_URL))
<div class="h-24 w-24 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 mr-2">
<img src="{{$product->custom_value1}}" alt="" class="h-full w-full object-cover object-center p-2">
<img src="{{$product->product_image}}" alt="" class="h-full w-full object-cover object-center p-2">
</div>
@endif
<div class="ml-0 flex flex-1 flex-col">
@ -190,7 +190,7 @@
<p class="mt-1 text-sm text-gray-500"></p>
</div>
<div class="flex justify-between text-sm mt-1">
@if(is_numeric($product->custom_value2))
@if(is_numeric($product->max_quantity))
<p class="text-gray-500 w-3/4"></p>
<div class="flex place-content-end">
@if($subscription->use_inventory_management && $product->in_stock_quantity == 0)
@ -200,7 +200,7 @@
@endif
<select wire:model.debounce.300ms="data.{{ $index }}.optional_qty" class="rounded-md border-gray-300 shadow-sm sm:text-sm">
<option value="0" selected="selected">0</option>
@for ($i = 1; $i <= ($subscription->use_inventory_management ? min($product->in_stock_quantity, min(100,$product->custom_value2)) : min(100,$product->custom_value2)); $i++)
@for ($i = 1; $i <= ($subscription->use_inventory_management ? min($product->in_stock_quantity, min(100,$product->max_quantity)) : min(100,$product->max_quantity)); $i++)
<option value="{{$i}}">{{$i}}</option>
@endfor
</select>

View file

@ -185,7 +185,7 @@ class BankTransactionRuleTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/bank_transaction_rules/'. $br->hashed_id, $data);
])->putJson('/api/v1/bank_transaction_rules/'. $br->hashed_id. '?include=expense_category', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
@ -194,7 +194,7 @@ class BankTransactionRuleTest extends TestCase
if($response){
$arr = $response->json();
nlog($arr);
$response->assertStatus(200);
}

View file

@ -43,6 +43,198 @@ class TaskApiTest extends TestCase
Model::reguard();
}
private function checkTimeLog(array $log): bool
{
if(count($log) == 0)
return true;
/*Get first value of all arrays*/
$result = array_column($log, 0);
/*Sort the array in ascending order*/
asort($result);
$new_array = [];
/*Rebuild the array in order*/
foreach($result as $key => $value)
$new_array[] = $log[$key];
/*Iterate through the array and perform checks*/
foreach($new_array as $key => $array)
{
/*Flag which helps us know if there is a NEXT timelog*/
$next = false;
/* If there are more than 1 time log in the array, ensure the last timestamp is not zero*/
if(count($new_array) >1 && $array[1] == 0)
return false;
/* Check if the start time is greater than the end time */
/* Ignore the last value for now, we'll do a separate check for this */
if($array[0] > $array[1] && $array[1] != 0)
return false;
/* Find the next time log value - if it exists */
if(array_key_exists($key+1, $new_array))
$next = $new_array[$key+1];
/* check the next time log and ensure the start time is GREATER than the end time of the previous record */
if($next && $next[0] < $array[1])
return false;
/* Get the last row of the timelog*/
$last_row = end($new_array);
/*If the last value is NOT zero, ensure start time is not GREATER than the endtime */
if($last_row[1] != 0 && $last_row[0] > $last_row[1])
return false;
return true;
}
}
public function testTimeLogChecker1()
{
$log = [
[50,0]
];
$this->assertTrue($this->checkTimeLog($log));
}
public function testTimeLogChecker2()
{
$log = [
[4,5],
[5,1]
];
$this->assertFalse($this->checkTimeLog($log));
}
public function testTimeLogChecker3()
{
$log = [
[4,5],
[3,50]
];
$this->assertFalse($this->checkTimeLog($log));
}
public function testTimeLogChecker4()
{
$log = [
[4,5],
[3,0]
];
$this->assertFalse($this->checkTimeLog($log));
}
public function testTimeLogChecker5()
{
$log = [
[4,5],
[3,1]
];
$this->assertFalse($this->checkTimeLog($log));
}
public function testTimeLogChecker6()
{
$log = [
[4,5],
[1,3],
];
$this->assertTrue($this->checkTimeLog($log));
}
public function testTimeLogChecker7()
{
$log = [
[1,3],
[4,5]
];
$this->assertTrue($this->checkTimeLog($log));
}
public function testTimeLogChecker8()
{
$log = [
[1,3],
[50,0]
];
$this->assertTrue($this->checkTimeLog($log));
}
public function testTimeLogChecker9()
{
$log = [
[4,5,'bb'],
[50,0,'aa'],
];
$this->assertTrue($this->checkTimeLog($log));
}
public function testTimeLogChecker10()
{
$log = [
[4,5,'5'],
[50,0,'3'],
];
$this->assertTrue($this->checkTimeLog($log));
}
public function testTimeLogChecker11()
{
$log = [
[1,2,'a'],
[3,4,'d'],
];
$this->assertTrue($this->checkTimeLog($log));
}
public function testTaskListClientStatus()
@ -58,7 +250,7 @@ class TaskApiTest extends TestCase
public function testTaskLockingGate()
{
$data = [
'timelog' => [[1,2],[3,4]],
'timelog' => [[1,2,'a'],[3,4,'d']],
];
$response = $this->withHeaders([
@ -194,7 +386,7 @@ class TaskApiTest extends TestCase
public function testTimeLogValidation3()
{
$data = [
'timelog' => [["a","b"],["c","d"]],
'timelog' => [["a","b",'d'],["c","d",'d']],
];
try {
@ -213,7 +405,7 @@ class TaskApiTest extends TestCase
public function testTimeLogValidation4()
{
$data = [
'timelog' => [[1,2],[3,0]],
'timelog' => [[1,2,'d'],[3,0,'d']],
];
$response = $this->withHeaders([
@ -232,8 +424,8 @@ class TaskApiTest extends TestCase
public function testStartTask()
{
$log = [
[2, 1],
[10, 20],
[2, 1,'d'],
[10, 20,'d'],
];
$last = end($log);

View file

@ -49,33 +49,6 @@ class WebhookAPITest extends TestCase
$this->withoutExceptionHandling();
}
// public function testClientWebhooks()
// {
// // client archived = 37
// $data = [
// 'target_url' => 'http://hook.com',
// 'event_id' => 37,
// 'rest_method' => 'post',
// 'format' => 'JSON',
// ];
// $response = $this->withHeaders([
// 'X-API-SECRET' => config('ninja.api_secret'),
// 'X-API-TOKEN' => $this->token,
// ])->post('/api/v1/webhooks', $data);
// $repo = new ClientRepository(new ClientContactRepository());
// $repo->archive($this->client);
// \Illuminate\Support\Facades\Queue::after(function (WebhookHandler $event) {
// $this->assertTrue($event->job->isReleased());
// });
// \Illuminate\Support\Facades\Queue::assertPushed(WebhookHandler::class);
// }
public function testWebhookGetFilter()
{
$response = $this->withHeaders([
@ -98,6 +71,20 @@ class WebhookAPITest extends TestCase
public function testWebhookPostRoute()
{
$data = [
'target_url' => 'http://hook.com',
'event_id' => 1,
'format' => 'JSON',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/webhooks', $data);
$response->assertStatus(200);
$data = [
'target_url' => 'http://hook.com',
'event_id' => 1,
@ -116,6 +103,20 @@ class WebhookAPITest extends TestCase
$this->assertEquals(1, $arr['data']['event_id']);
$data = [
'target_url' => 'http://hook.com',
'event_id' => 2,
'format' => 'JSON',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/webhooks/'.$arr['data']['id'], $data);
$response->assertStatus(200);
$data = [
'target_url' => 'http://hook.com',
'event_id' => 2,