Merge pull request #8258 from turbo124/v5-develop

Attach recurring invoice docs
This commit is contained in:
David Bomba 2023-02-07 23:39:01 +11:00 committed by GitHub
commit eff0945c08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 122 additions and 42 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

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

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

@ -163,9 +163,9 @@ class BankTransactionRule extends BaseModel
return $this->belongsTo(User::class)->withTrashed();
}
public function expense_cateogry()
public function expense_category()
{
return $this->belongsTo(ExpenseCategory::class)->withTrashed();
return $this->belongsTo(ExpenseCategory::class, 'category_id', 'id')->withTrashed();
}
}

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

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

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

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

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

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,13 +11,12 @@
namespace App\Transformers;
use App\Models\Account;
use App\Models\BankTransaction;
use App\Models\BankTransactionRule;
use App\Models\Company;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\ExpenseCategory;
use App\Transformers\VendorTransformer;
use App\Transformers\ExpenseCateogryTransformer;
use App\Utils\Traits\MakesHash;
/**
@ -77,6 +76,9 @@ class BankTransactionRuleTransformer extends EntityTransformer
{
$transformer = new ClientTransformer($this->serializer);
if(!$bank_transaction_rule->client)
return null;
return $this->includeItem($bank_transaction_rule->expense, $transformer, Client::class);
}
@ -84,7 +86,20 @@ class BankTransactionRuleTransformer extends EntityTransformer
{
$transformer = new VendorTransformer($this->serializer);
if(!$bank_transaction_rule->vendor)
return null;
return $this->includeItem($bank_transaction_rule->vendor, $transformer, Vendor::class);
}
public function includeExpenseCategory(BankTransactionRule $bank_transaction_rule)
{
$transformer = new ExpenseCategoryTransformer($this->serializer);
if(!$bank_transaction_rule->expense_cateogry)
return null;
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

@ -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,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('products', function (Blueprint $table){
$table->unsignedInteger("max_quantity")->nullable();
$table->string("product_image", 191)->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
};

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()})();

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

@ -146,7 +146,7 @@ class Approve {
});
}
if (!this.shouldDisplaySignature && !this.shouldDisplayTerms && this.shouldDisplayUserInput) {
if (!this.shouldDisplaySignature && !this.shouldDisplayTerms && !this.shouldDisplayUserInput) {
this.submitForm();
}
});

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

@ -58,7 +58,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 +194,7 @@ class TaskApiTest extends TestCase
public function testTimeLogValidation3()
{
$data = [
'timelog' => [["a","b"],["c","d"]],
'timelog' => [["a","b",'d'],["c","d",'d']],
];
try {
@ -213,7 +213,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 +232,8 @@ class TaskApiTest extends TestCase
public function testStartTask()
{
$log = [
[2, 1],
[10, 20],
[2, 1,'d'],
[10, 20,'d'],
];
$last = end($log);