Merge pull request #8719 from turbo124/v5-develop

v5.6.31
This commit is contained in:
David Bomba 2023-08-16 15:03:04 +10:00 committed by GitHub
commit 035300bfcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1058 additions and 567 deletions

View File

@ -1 +1 @@
5.6.30 5.6.31

View File

@ -471,7 +471,7 @@ class CheckData extends Command
$ii->saveQuietly(); $ii->saveQuietly();
}); });
collect([Invoice::class, Quote::class, Credit::class, PurchaseOrder::class])->each(function ($entity) { collect([Invoice::class, Quote::class, Credit::class, PurchaseOrder::class, RecurringInvoice::class])->each(function ($entity) {
if ($entity::doesntHave('invitations')->count() > 0) { if ($entity::doesntHave('invitations')->count() > 0) {
$entity::doesntHave('invitations')->cursor()->each(function ($entity) { $entity::doesntHave('invitations')->cursor()->each(function ($entity) {
$client_vendor_key = 'client_id'; $client_vendor_key = 'client_id';
@ -694,7 +694,7 @@ class CheckData extends Command
{ {
$this->wrong_balances = 0; $this->wrong_balances = 0;
Client::cursor()->where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->each(function ($client) { Client::query()->cursor()->where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->each(function ($client) {
$client->invoices->where('is_deleted', false)->whereIn('status_id', '!=', Invoice::STATUS_DRAFT)->each(function ($invoice) use ($client) { $client->invoices->where('is_deleted', false)->whereIn('status_id', '!=', Invoice::STATUS_DRAFT)->each(function ($invoice) use ($client) {
$total_paid = $invoice->payments() $total_paid = $invoice->payments()
->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED]) ->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
@ -876,7 +876,7 @@ class CheckData extends Command
$this->wrong_balances = 0; $this->wrong_balances = 0;
$this->wrong_paid_to_dates = 0; $this->wrong_paid_to_dates = 0;
foreach (Client::where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->cursor() as $client) { foreach (Client::query()->where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->cursor() as $client) {
$invoice_balance = $client->invoices()->where('is_deleted', false)->whereIn('status_id', [2,3])->sum('balance'); $invoice_balance = $client->invoices()->where('is_deleted', false)->whereIn('status_id', [2,3])->sum('balance');
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first(); $ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();

View File

@ -0,0 +1,101 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Analytics;
use Turbo124\Beacon\ExampleMetric\GenericMixedMetric;
class RevenueTrack extends GenericMixedMetric
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'mixed_metric';
/**
* The name of the counter.
* @var string
*/
public $name = 'app.revenue';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
*/
public $datetime;
/**
* The Client email
*
* @var string
*/
public $string_metric5 = 'email';
/**
* The AccountKey email
*
* @var string
*/
public $string_metric6 = 'key';
/**
* Product Type
*
* @var string
*/
public $string_metric7 = 'product';
/**
* Gateway Reference
*
* @var string
*/
public $string_metric8 = 'gateway_reference';
public $string_metric9 = 'entity_reference';
public $string_metric10 = 'gateway_type';
/**
* The counter
* set to 1.
*
* @var int
*/
public $int_metric1 = 1;
/**
* Amount Received
*
* @var double
*/
public $double_metric2 = 0;
public function __construct($string_metric5, $string_metric6, $int_metric1, $double_metric2, $string_metric7, $string_metric8, $string_metric9, $string_metric10)
{
$this->int_metric1 = $int_metric1;
$this->double_metric2 = $double_metric2;
$this->string_metric5 = $string_metric5;
$this->string_metric6 = $string_metric6;
$this->string_metric7 = $string_metric7;
$this->string_metric8 = $string_metric8;
$this->string_metric9 = $string_metric9;
$this->string_metric10 = $string_metric10;
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Events\Account;
use App\Models\Company;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
/**
* Class StripeConnectFailure.
*/
class StripeConnectFailure
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Company $company, public string $db)
{
}
public function broadcastOn()
{
return [];
}
}

View File

@ -88,6 +88,11 @@ class CreditFilters extends QueryFilters
->orWhere('credits.custom_value4', 'like', '%'.$filter.'%') ->orWhere('credits.custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) { ->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%'); $q->where('name', 'like', '%'.$filter.'%');
})
->orWhereHas('client.contacts', function ($q) use ($filter) {
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
}); });
}); });
} }

View File

@ -116,6 +116,11 @@ class InvoiceFilters extends QueryFilters
->orWhere('custom_value4', 'like', '%'.$filter.'%') ->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) { ->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%'); $q->where('name', 'like', '%'.$filter.'%');
})
->orWhereHas('client.contacts', function ($q) use ($filter) {
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
}); });
}); });
} }

View File

@ -44,6 +44,11 @@ class PaymentFilters extends QueryFilters
->orWhere('custom_value4', 'like', '%'.$filter.'%') ->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) { ->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%'); $q->where('name', 'like', '%'.$filter.'%');
})
->orWhereHas('client.contacts', function ($q) use ($filter) {
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
}); });
}); });
} }

View File

@ -40,6 +40,11 @@ class QuoteFilters extends QueryFilters
->orWhere('custom_value4', 'like', '%'.$filter.'%') ->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) { ->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%'); $q->where('name', 'like', '%'.$filter.'%');
})
->orWhereHas('client.contacts', function ($q) use ($filter) {
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
}); });
}); });
} }

View File

@ -42,6 +42,11 @@ class RecurringInvoiceFilters extends QueryFilters
->orWhere('custom_value4', 'like', '%'.$filter.'%') ->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) { ->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%'); $q->where('name', 'like', '%'.$filter.'%');
})
->orWhereHas('client.contacts', function ($q) use ($filter) {
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
}); });
}); });
} }

View File

@ -45,6 +45,11 @@ class TaskFilters extends QueryFilters
}) })
->orWhereHas('client', function ($q) use ($filter) { ->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%'); $q->where('name', 'like', '%'.$filter.'%');
})
->orWhereHas('client.contacts', function ($q) use ($filter) {
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
}); });
}); });
} }

View File

@ -162,7 +162,9 @@ class InvoiceController extends BaseController
*/ */
public function create(CreateInvoiceRequest $request) public function create(CreateInvoiceRequest $request)
{ {
$invoice = InvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id); /** @var \App\Models\User $user */
$user = auth()->user();
$invoice = InvoiceFactory::create($user->company()->id, $user->id);
return $this->itemResponse($invoice); return $this->itemResponse($invoice);
} }
@ -211,7 +213,11 @@ class InvoiceController extends BaseController
*/ */
public function store(StoreInvoiceRequest $request) public function store(StoreInvoiceRequest $request)
{ {
$invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id));
/** @var \App\Models\User $user */
$user = auth()->user();
$invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create($user->company()->id, $user->id));
$invoice = $invoice->service() $invoice = $invoice->service()
->fillDefaults() ->fillDefaults()
@ -219,7 +225,7 @@ class InvoiceController extends BaseController
->adjustInventory() ->adjustInventory()
->save(); ->save();
event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars($user ? $user->id : null)));
$transaction = [ $transaction = [
'invoice' => $invoice->transaction_event(), 'invoice' => $invoice->transaction_event(),
@ -473,62 +479,17 @@ class InvoiceController extends BaseController
return $this->itemResponse($invoice->fresh()); return $this->itemResponse($invoice->fresh());
} }
/**
* Perform bulk actions on the list view.
*
* @return \Illuminate\Support\Collection
*
* @OA\Post(
* path="/api/v1/invoices/bulk",
* operationId="bulkInvoices",
* tags={"invoices"},
* summary="Performs bulk actions on an array of invoices",
* description="",
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/index"),
* @OA\RequestBody(
* description="User credentials",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(
* type="array",
* @OA\Items(
* type="integer",
* description="Array of hashed IDs to be bulk 'actioned",
* example="[0,1,2,3]",
* ),
* )
* )
* ),
* @OA\Response(
* response=200,
* description="The Bulk Action response",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function bulk(BulkInvoiceRequest $request) public function bulk(BulkInvoiceRequest $request)
{ {
/** @var \App\Models\User $user */
$user = auth()->user();
$action = $request->input('action'); $action = $request->input('action');
$ids = $request->input('ids'); $ids = $request->input('ids');
if (Ninja::isHosted() && (stripos($action, 'email') !== false) && !auth()->user()->company()->account->account_sms_verified) { if (Ninja::isHosted() && (stripos($action, 'email') !== false) && !$user->company()->account->account_sms_verified) {
return response(['message' => 'Please verify your account to send emails.'], 400); return response(['message' => 'Please verify your account to send emails.'], 400);
} }
@ -543,8 +504,8 @@ class InvoiceController extends BaseController
*/ */
if ($action == 'bulk_download' && $invoices->count() > 1) { if ($action == 'bulk_download' && $invoices->count() > 1) {
$invoices->each(function ($invoice) { $invoices->each(function ($invoice) use($user) {
if (auth()->user()->cannot('view', $invoice)) { if ($user->cannot('view', $invoice)) {
nlog('access denied'); nlog('access denied');
return response()->json(['message' => ctrans('text.access_denied')]); return response()->json(['message' => ctrans('text.access_denied')]);
@ -556,7 +517,7 @@ class InvoiceController extends BaseController
return response()->json(['message' => ctrans('texts.sent_message')], 200); return response()->json(['message' => ctrans('texts.sent_message')], 200);
} }
if ($action == 'download' && $invoices->count() >=1 && auth()->user()->can('view', $invoices->first())) { if ($action == 'download' && $invoices->count() >=1 && $user->can('view', $invoices->first())) {
$file = $invoices->first()->service()->getInvoicePdf(); $file = $invoices->first()->service()->getInvoicePdf();
return response()->streamDownload(function () use ($file) { return response()->streamDownload(function () use ($file) {
@ -564,7 +525,7 @@ class InvoiceController extends BaseController
}, basename($file), ['Content-Type' => 'application/pdf']); }, basename($file), ['Content-Type' => 'application/pdf']);
} }
if ($action == 'bulk_print' && auth()->user()->can('view', $invoices->first())) { if ($action == 'bulk_print' && $user->can('view', $invoices->first())) {
$paths = $invoices->map(function ($invoice) { $paths = $invoices->map(function ($invoice) {
return $invoice->service()->getInvoicePdf(); return $invoice->service()->getInvoicePdf();
}); });
@ -579,15 +540,15 @@ class InvoiceController extends BaseController
/* /*
* Send the other actions to the switch * Send the other actions to the switch
*/ */
$invoices->each(function ($invoice, $key) use ($action) { $invoices->each(function ($invoice, $key) use ($action, $user) {
if (auth()->user()->can('edit', $invoice)) { if ($user->can('edit', $invoice)) {
$this->performAction($invoice, $action, true); $this->performAction($invoice, $action, true);
} }
}); });
/* Need to understand which permission are required for the given bulk action ie. view / edit */ /* Need to understand which permission are required for the given bulk action ie. view / edit */
return $this->listResponse(Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()); return $this->listResponse(Invoice::query()->withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
} }
/** /**
@ -1005,16 +966,17 @@ class InvoiceController extends BaseController
*/ */
public function upload(UploadInvoiceRequest $request, Invoice $invoice) public function upload(UploadInvoiceRequest $request, Invoice $invoice)
{ {
if (! $this->checkFeature(Account::FEATURE_DOCUMENTS)) { if (! $this->checkFeature(Account::FEATURE_DOCUMENTS)) {
return $this->featureFailure(); return $this->featureFailure();
} }
if ($request->has('documents')) { if ($request->has('documents')) {
$this->saveDocuments($request->file('documents'), $invoice); $this->saveDocuments($request->file('documents'), $invoice, $request->input('is_public', true));
} }
if ($request->has('file')) { if ($request->has('file')) {
$this->saveDocuments($request->file('documents'), $invoice); $this->saveDocuments($request->file('documents'), $invoice, $request->input('is_public', true));
} }
return $this->itemResponse($invoice->fresh()); return $this->itemResponse($invoice->fresh());
@ -1022,7 +984,10 @@ class InvoiceController extends BaseController
public function update_reminders(UpdateReminderRequest $request) public function update_reminders(UpdateReminderRequest $request)
{ {
UpdateReminders::dispatch(auth()->user()->company()); /** @var \App\Models\User $user */
$user = auth()->user();
UpdateReminders::dispatch($user->company());
return response()->json(['message' => 'Updating reminders'], 200); return response()->json(['message' => 'Updating reminders'], 200);
} }

View File

@ -62,11 +62,11 @@ class BillingPortalPurchase extends Component
public $password; public $password;
/** /**
* Instance of subscription. * This arrives as an int and we resolve in the mount method
* *
* @var \App\Models\Subscription $subscription * @var int|Subscription
*/ */
public Subscription $subscription; public $subscription;
/** /**
* Instance of client contact. * Instance of client contact.

View File

@ -12,6 +12,7 @@
namespace App\Http\Livewire; namespace App\Http\Livewire;
use App\Services\Invoice\GetInvoiceEInvoice;
use App\Utils\Number; use App\Utils\Number;
use Livewire\Component; use Livewire\Component;
use App\Utils\HtmlEngine; use App\Utils\HtmlEngine;
@ -95,6 +96,20 @@ class PdfSlot extends Component
echo $file; echo $file;
}, $file_name, $headers); }, $file_name, $headers);
}
public function downloadEInvoice()
{
$file_name = $this->entity->numberFormatter().'.xml';
$file = (new GetInvoiceEInvoice($this->entity))->run();
$headers = ['Content-Type' => 'application/xml'];
return response()->streamDownload(function () use ($file) {
echo $file;
}, $file_name, $headers);
} }
public function render() public function render()
@ -106,6 +121,7 @@ class PdfSlot extends Component
$this->show_cost = in_array('$product.unit_cost', $this->settings->pdf_variables->product_columns); $this->show_cost = in_array('$product.unit_cost', $this->settings->pdf_variables->product_columns);
$this->show_line_total = in_array('$product.line_total', $this->settings->pdf_variables->product_columns); $this->show_line_total = in_array('$product.line_total', $this->settings->pdf_variables->product_columns);
$this->show_quantity = in_array('$product.quantity', $this->settings->pdf_variables->product_columns);
if($this->entity_type == 'quote' && !$this->settings->sync_invoice_quote_columns ){ if($this->entity_type == 'quote' && !$this->settings->sync_invoice_quote_columns ){
$this->show_cost = in_array('$product.unit_cost', $this->settings->pdf_variables->product_quote_columns); $this->show_cost = in_array('$product.unit_cost', $this->settings->pdf_variables->product_quote_columns);

View File

@ -23,7 +23,9 @@ class UploadInvoiceRequest extends Request
*/ */
public function authorize() : bool public function authorize() : bool
{ {
return auth()->user()->can('edit', $this->invoice); /** @var \App\Models\User $user */
$user = auth()->user();
return $user->can('edit', $this->invoice);
} }
public function rules() public function rules()
@ -42,6 +44,8 @@ class UploadInvoiceRequest extends Request
$rules['file'] = $this->file_validation; $rules['file'] = $this->file_validation;
} }
$rules['is_public'] = 'sometimes|boolean';
return $rules; return $rules;
} }

View File

@ -20,7 +20,7 @@ class Request extends FormRequest
use MakesHash; use MakesHash;
use RuntimeFormRequest; use RuntimeFormRequest;
protected $file_validation = 'sometimes|file|mimes:png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx,webp|max:20000'; protected $file_validation = 'sometimes|file|mimes:png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx,webp,xml|max:20000';
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *

View File

@ -13,6 +13,7 @@ namespace App\Jobs\Entity;
use App\Exceptions\FilePermissionsFailure; use App\Exceptions\FilePermissionsFailure;
use App\Jobs\Invoice\CreateEInvoice; use App\Jobs\Invoice\CreateEInvoice;
use App\Jobs\Invoice\MergeEInvoice;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Models\Credit; use App\Models\Credit;
use App\Models\CreditInvitation; use App\Models\CreditInvitation;
@ -214,7 +215,9 @@ class CreateEntityPdf implements ShouldQueue
} }
} }
if ($this->entity_string == "invoice" && $this->client->getSetting('enable_e_invoice')){ if ($this->entity_string == "invoice" && $this->client->getSetting('enable_e_invoice')){
(new CreateEInvoice($this->entity, true))->handle(); (new CreateEInvoice($this->entity))->handle();
(new MergeEInvoice($this->entity))->handle();
} }
$this->invitation = null; $this->invitation = null;
// $this->entity = null; // $this->entity = null;

View File

@ -12,6 +12,7 @@
namespace App\Jobs\Entity; namespace App\Jobs\Entity;
use App\Exceptions\FilePermissionsFailure; use App\Exceptions\FilePermissionsFailure;
use App\Jobs\Invoice\MergeEInvoice;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Models\Credit; use App\Models\Credit;
use App\Models\CreditInvitation; use App\Models\CreditInvitation;
@ -202,6 +203,12 @@ class CreateRawPdf implements ShouldQueue
if ($pdf) { if ($pdf) {
$maker =null; $maker =null;
$state = null; $state = null;
if ($this->invitation->invoice->client->getSetting('enable_e_invoice') && $this->entity_string == "invoice"){
$filename = tempnam(sys_get_temp_dir(), 'InvoiceNinja').".pdf";
file_put_contents($filename, $pdf);
(new \App\Services\Invoice\MergeEInvoice($this->invitation->invoice, $filename))->run();
return file_get_contents($filename);
};
return $pdf; return $pdf;
} }

View File

@ -14,6 +14,7 @@ namespace App\Jobs\Invoice;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Models\Invoice; use App\Models\Invoice;
use horstoeko\zugferd\ZugferdDocumentBuilder;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
@ -29,7 +30,7 @@ class CreateEInvoice implements ShouldQueue
public $deleteWhenMissingModels = true; public $deleteWhenMissingModels = true;
public function __construct(private Invoice $invoice, private bool $alterPDF, private string $custom_pdf_path = "") public function __construct(private Invoice $invoice, private bool $returnObject = false)
{ {
} }
@ -39,7 +40,7 @@ class CreateEInvoice implements ShouldQueue
* *
* @return string * @return string
*/ */
public function handle(): string public function handle(): string|ZugferdDocumentBuilder
{ {
/* Forget the singleton*/ /* Forget the singleton*/
App::forgetInstance('translator'); App::forgetInstance('translator');
@ -63,13 +64,13 @@ class CreateEInvoice implements ShouldQueue
case "XInvoice-Extended": case "XInvoice-Extended":
case "XInvoice-BasicWL": case "XInvoice-BasicWL":
case "XInvoice-Basic": case "XInvoice-Basic":
return (new ZugferdEInvoice($this->invoice))->run(); return (new ZugferdEInvoice($this->invoice, $this->returnObject))->run();
case "Facturae_3.2": case "Facturae_3.2":
case "Facturae_3.2.1": case "Facturae_3.2.1":
case "Facturae_3.2.2": case "Facturae_3.2.2":
return (new FacturaEInvoice($this->invoice, str_replace("Facturae_", "", $e_invoice_type)))->run(); return (new FacturaEInvoice($this->invoice, str_replace("Facturae_", "", $e_invoice_type)))->run();
default: default:
return (new ZugferdEInvoice($this->invoice))->run(); return (new ZugferdEInvoice($this->invoice, $this->returnObject))->run();
} }

View File

@ -0,0 +1,68 @@
<?php
namespace App\Jobs\Invoice;
use App\Models\ClientContact;
use App\Models\Invoice;
use horstoeko\zugferd\ZugferdDocumentPdfBuilder;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Storage;
use horstoeko\zugferd\ZugferdDocumentReader;
class MergeEInvoice implements ShouldQueue
{
public function __construct(public Invoice $invoice, private string $pdf_path = "")
{
}
/**
* @throws \Exception
*/
public function handle(): void
{
$e_invoice_type = $this->invoice->client->getSetting('e_invoice_type');
switch ($e_invoice_type) {
case "EN16931":
case "XInvoice_2_2":
case "XInvoice_2_1":
case "XInvoice_2_0":
case "XInvoice_1_0":
case "XInvoice-Extended":
case "XInvoice-BasicWL":
case "XInvoice-Basic":
$this->embedEInvoiceZuGFerD();
//case "Facturae_3.2":
//case "Facturae_3.2.1":
//case "Facturae_3.2.2":
//
default:
$this->embedEInvoiceZuGFerD();
break;
}
}
/**
* @throws \Exception
*/
private function embedEInvoiceZuGFerD(): void
{
$filepath_pdf = !empty($this->pdf_path) ? $this->pdf_path : $this->invoice->service()->getInvoicePdf();
$disk = config('filesystems.default');
$e_rechnung = (new CreateEInvoice($this->invoice, true))->handle();
if (!empty($this->pdf_path)){
$realpath_pdf = $filepath_pdf;
}
else {
$realpath_pdf = Storage::disk($disk)->path($filepath_pdf);
}
if (file_exists($realpath_pdf)){
$pdfBuilder = new ZugferdDocumentPdfBuilder($e_rechnung, $realpath_pdf);
$pdfBuilder->generateDocument();
$pdfBuilder->saveDocument($realpath_pdf);
}
else{
nlog("E_Invoice Merge failed - file to merge not found");
}
}
}

View File

@ -76,7 +76,8 @@ class ZipInvoices implements ShouldQueue
$this->invoices->each(function ($invoice) { $this->invoices->each(function ($invoice) {
(new CreateEntityPdf($invoice->invitations()->first()))->handle(); (new CreateEntityPdf($invoice->invitations()->first()))->handle();
if ($invoice->client->getSetting('enable_e_invoice')){ if ($invoice->client->getSetting('enable_e_invoice')){
(new CreateEInvoice($invoice, false))->handle(); (new CreateEInvoice($invoice))->handle();
(new MergeEInvoice($invoice))->handle();
} }
}); });

View File

@ -36,10 +36,10 @@ class UserEmailChanged implements ShouldQueue
* Create a new job instance. * Create a new job instance.
* *
* @param \App\Models\User $new_user * @param \App\Models\User $new_user
* @param \App\Models\User $old_user * @param \stdClass $old_user
* @param \App\Models\Company $company * @param \App\Models\Company $company
*/ */
public function __construct(protected User $new_user, protected User $old_user, protected Company $company) public function __construct(protected User $new_user, protected \stdClass $old_user, protected Company $company)
{ {
$this->settings = $this->company->settings; $this->settings = $this->company->settings;
} }

View File

@ -1821,7 +1821,7 @@ class Import implements ShouldQueue
private function processActivities(array $data): void private function processActivities(array $data): void
{ {
Activity::where('company_id', $this->company->id)->cursor()->each(function ($a){ Activity::query()->where('company_id', $this->company->id)->cursor()->each(function ($a){
$a->forceDelete(); $a->forceDelete();
nlog("deleting {$a->id}"); nlog("deleting {$a->id}");
}); });

View File

@ -53,7 +53,7 @@ class UploadFile implements ShouldQueue
public $disk; public $disk;
public function __construct($file, $type, $user, $company, $entity, $disk = null, $is_public = false) public function __construct($file, $type, $user, $company, $entity, $disk = null, $is_public = true)
{ {
$this->file = $file; $this->file = $file;
$this->type = $type; $this->type = $type;

View File

@ -0,0 +1,57 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Listeners\Account;
use App\Utils\Ninja;
use App\Libraries\MultiDB;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use Illuminate\Support\Facades\Cache;
use App\Mail\Ninja\StripeConnectFailed;
use Illuminate\Contracts\Queue\ShouldQueue;
class StripeConnectFailureListener implements ShouldQueue
{
/**
* Create the event listener.
*
*/
public function __construct()
{
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
MultiDB::setDb($event->db);
if (Ninja::isHosted() && is_null(Cache::get("stripe_connect_notification:{$event->company->company_key}")))
{
$nmo = new NinjaMailerObject();
$nmo->mailable = new StripeConnectFailed($event->company->owner(), $event->company);
$nmo->company = $event->company;
$nmo->settings = $event->company->settings;
$nmo->to_user = $event->company->owner();
NinjaMailerJob::dispatch($nmo, true);
Cache::put("stripe_connect_notification:{$event->company->company_key}", true, 60 * 60 * 24);
}
}
}

View File

@ -21,6 +21,7 @@ class PaymentCreatedActivity implements ShouldQueue
{ {
protected $activity_repo; protected $activity_repo;
public $delay = 5;
/** /**
* Create the event listener. * Create the event listener.
* *

View File

@ -21,6 +21,8 @@ class InvoicePaidActivity implements ShouldQueue
{ {
protected $activity_repo; protected $activity_repo;
public $delay = 10;
/** /**
* Create the event listener. * Create the event listener.
* *

View File

@ -11,14 +11,16 @@
namespace App\Listeners\Payment; namespace App\Listeners\Payment;
use App\Utils\Ninja;
use App\Libraries\MultiDB;
use App\Jobs\Mail\NinjaMailer; use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob; use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject; use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\Admin\EntityPaidObject; use App\Mail\Admin\EntityPaidObject;
use App\Utils\Ninja; use Turbo124\Beacon\Facades\LightLogs;
use App\Utils\Traits\Notifications\UserNotifies; use App\DataMapper\Analytics\RevenueTrack;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use App\Utils\Traits\Notifications\UserNotifies;
class PaymentNotification implements ShouldQueue class PaymentNotification implements ShouldQueue
{ {
@ -158,6 +160,15 @@ class PaymentNotification implements ShouldQueue
$url = $base."&t=item&in={$item}&ip={$amount}&iq=1"; $url = $base."&t=item&in={$item}&ip={$amount}&iq=1";
$this->sendAnalytics($url); $this->sendAnalytics($url);
$email = $client->present()->email();
$account_key = $client->custom_value2 ?? 'unknown';
$product = $item;
$gateway_reference = $client->gateway_tokens()->count() >= 1 ? ($client->gateway_tokens()->first()->gateway_customer_reference ?? '') : '';
// LightLogs::create(new RevenueTrack($email, $account_key, 1, $amount, $product, $gateway_reference, $entity_number))
// ->batch();
} }
/** /**

View File

@ -0,0 +1,114 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Mail\Ninja;
use App\Models\User;
use App\Models\Company;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Headers;
use Illuminate\Mail\Mailables\Envelope;
class StripeConnectFailed extends Mailable
{
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(public User $user, public Company $company)
{
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: "Stripe Connect not configured, please login and connect.",
from: "maildelivery@invoicing.co",
to: $this->user->email,
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'email.admin.stripe_connect_failed',
text: 'email.admin.stripe_connect_failed_text',
with: [
'text_body' => $this->textBody(), //@todo this is a bit hacky here.
'body' => $this->htmlBody(),
'title' => 'Connect your Stripe account',
'settings' => $this->company->settings,
'logo' => $this->company->present()->logo(),
'signature' => '',
'company' => $this->company,
'greeting' => '',
'links' => [],
'url' => 'https://www.loom.com/share/a3dc3131cc924e14a34634d5d48065c8?sid=b1971aa2-9deb-4339-8ebd-53f9947ef633',
'button' => "texts.view"
]
);
}
private function textBody()
{
return "
We note you are yet to connect your Stripe account to Invoice Ninja. Please log in to Invoice Ninja and connect your Stripe account.\n\n
Once logged in you can use the following resource to connect your Stripe account: \n\n
";
}
private function htmlBody()
{
return "
We note you are yet to connect your Stripe account to Invoice Ninja. Please log in to Invoice Ninja and connect your Stripe account.<br><br>
Once logged in you can use the following resource to connect your Stripe account: <br><br>
";
}
/**
* Get the attachments for the message.
*
* @return array
*/
public function attachments()
{
return [];
}
/**
* Get the message headers.
*
* @return \Illuminate\Mail\Mailables\Headers
*/
public function headers()
{
return new Headers(
messageId: null,
references: [],
text:['' => ''],
);
}
}

View File

@ -707,6 +707,7 @@ class Company extends BaseModel
public function getSetting($setting) public function getSetting($setting)
{ {
//todo $this->setting ?? false
if (property_exists($this->settings, $setting) != false) { if (property_exists($this->settings, $setting) != false) {
return $this->settings->{$setting}; return $this->settings->{$setting};
} }

View File

@ -20,7 +20,7 @@ use Illuminate\Database\Eloquent\Model;
* @property string $hash * @property string $hash
* @property float $fee_total * @property float $fee_total
* @property int|null $fee_invoice_id * @property int|null $fee_invoice_id
* @property mixed $data * @property \stdClass $data
* @property int|null $payment_id * @property int|null $payment_id
* @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $updated_at
@ -38,9 +38,16 @@ class PaymentHash extends Model
'data' => 'object', 'data' => 'object',
]; ];
/**
* @class \App\Models\PaymentHash $this
* @property \App\Models\PaymentHash $data
* @class \stdClass $data
* @property string $raw_value
*/
/** /**
* @return array * @return mixed
*/ */
public function invoices() public function invoices()
{ {
@ -75,9 +82,8 @@ class PaymentHash extends Model
public function withData(string $property, $value): self public function withData(string $property, $value): self
{ {
$this->data = array_merge((array) $this->data, [$property => $value]); $this->data = array_merge((array) $this->data, [$property => $value]); // @phpstan-ignore-line
$this->save(); $this->save();// @phpstan-ignore-line
return $this; // @phpstan-ignore-line
return $this;
} }
} }

View File

@ -230,9 +230,9 @@ class Vendor extends BaseModel
* Returns a vendor settings proxying company setting * Returns a vendor settings proxying company setting
* *
* @param string $setting * @param string $setting
* @return string * @return mixed
*/ */
public function getSetting($setting): string public function getSetting($setting): mixed
{ {
if ((property_exists($this->company->settings, $setting) != false) && (isset($this->company->settings->{$setting}) !== false)) { if ((property_exists($this->company->settings, $setting) != false) && (isset($this->company->settings->{$setting}) !== false)) {
return $this->company->settings->{$setting}; return $this->company->settings->{$setting};

View File

@ -69,7 +69,7 @@ class CheckoutWebhook implements ShouldQueue
{ {
$payment_object = $this->webhook_array['data']; $payment_object = $this->webhook_array['data'];
$payment = Payment::withTrashed()->where('transaction_reference', $payment_object['id'])->first(); $payment = Payment::query()->withTrashed()->where('transaction_reference', $payment_object['id'])->first();
if($payment && $payment->status_id == Payment::STATUS_COMPLETED) if($payment && $payment->status_id == Payment::STATUS_COMPLETED)
return; return;
@ -84,18 +84,19 @@ class CheckoutWebhook implements ShouldQueue
$metadata = $this->webhook_array['metadata']; $metadata = $this->webhook_array['metadata'];
$payment_hash = PaymentHash::where('hash', $metadata['udf2'])->first(); $payment_hash = PaymentHash::query()->where('hash', $metadata['udf2'])->first();
$driver = $this->company_gateway->driver($payment_hash->fee_invoice->client)->init()->setPaymentMethod(); $driver = $this->company_gateway->driver($payment_hash->fee_invoice->client)->init()->setPaymentMethod();
$payment_hash->data = array_merge((array) $payment_hash->data, $this->webhook_array); $payment_hash->data = array_merge((array) $payment_hash->data, $this->webhook_array); // @phpstan-ignore-line
$payment_hash->save(); $payment_hash->save();
$driver->setPaymentHash($payment_hash); $driver->setPaymentHash($payment_hash);
// @phpstan-ignore-line
$data = [ $data = [
'payment_method' => isset($this->webhook_array['source']['id']) ? $this->webhook_array['source']['id'] : '', 'payment_method' => isset($this->webhook_array['source']['id']) ? $this->webhook_array['source']['id'] : '',
'payment_type' => PaymentType::CREDIT_CARD_OTHER, 'payment_type' => PaymentType::CREDIT_CARD_OTHER,
'amount' => $payment_hash->data->raw_value, 'amount' => $payment_hash->data->raw_value, // @phpstan-ignore-line
'transaction_reference' => $payment_object['id'], 'transaction_reference' => $payment_object['id'],
'gateway_type_id' => GatewayType::CREDIT_CARD, 'gateway_type_id' => GatewayType::CREDIT_CARD,
]; ];

View File

@ -66,8 +66,6 @@ class Webhook
/** /**
* Lists the workflows in Checkout * Lists the workflows in Checkout
*
* @return void
*/ */
public function getWorkFlows() public function getWorkFlows()
{ {

View File

@ -94,6 +94,7 @@ use App\Events\Misc\InvitationWasViewed;
use App\Events\Payment\PaymentWasVoided; use App\Events\Payment\PaymentWasVoided;
use App\Events\Vendor\VendorWasArchived; use App\Events\Vendor\VendorWasArchived;
use App\Events\Vendor\VendorWasRestored; use App\Events\Vendor\VendorWasRestored;
use App\Events\Account\StripeConnectFailure;
use App\Listeners\Mail\MailSentListener; use App\Listeners\Mail\MailSentListener;
use App\Observers\ClientContactObserver; use App\Observers\ClientContactObserver;
use App\Observers\PurchaseOrderObserver; use App\Observers\PurchaseOrderObserver;
@ -198,6 +199,8 @@ use App\Listeners\Invoice\InvoiceRestoredActivity;
use App\Listeners\Invoice\InvoiceReversedActivity; use App\Listeners\Invoice\InvoiceReversedActivity;
use App\Listeners\Payment\PaymentRestoredActivity; use App\Listeners\Payment\PaymentRestoredActivity;
use App\Listeners\Quote\QuoteApprovedNotification; use App\Listeners\Quote\QuoteApprovedNotification;
use SocialiteProviders\Apple\AppleExtendSocialite;
use SocialiteProviders\Manager\SocialiteWasCalled;
use App\Events\Subscription\SubscriptionWasCreated; use App\Events\Subscription\SubscriptionWasCreated;
use App\Events\Subscription\SubscriptionWasDeleted; use App\Events\Subscription\SubscriptionWasDeleted;
use App\Events\Subscription\SubscriptionWasUpdated; use App\Events\Subscription\SubscriptionWasUpdated;
@ -221,10 +224,12 @@ use App\Listeners\Invoice\InvoiceEmailFailedActivity;
use App\Events\PurchaseOrder\PurchaseOrderWasAccepted; use App\Events\PurchaseOrder\PurchaseOrderWasAccepted;
use App\Events\PurchaseOrder\PurchaseOrderWasArchived; use App\Events\PurchaseOrder\PurchaseOrderWasArchived;
use App\Events\PurchaseOrder\PurchaseOrderWasRestored; use App\Events\PurchaseOrder\PurchaseOrderWasRestored;
use App\Listeners\Payment\PaymentEmailFailureActivity;
use App\Listeners\Vendor\UpdateVendorContactLastLogin; use App\Listeners\Vendor\UpdateVendorContactLastLogin;
use App\Events\RecurringQuote\RecurringQuoteWasCreated; use App\Events\RecurringQuote\RecurringQuoteWasCreated;
use App\Events\RecurringQuote\RecurringQuoteWasDeleted; use App\Events\RecurringQuote\RecurringQuoteWasDeleted;
use App\Events\RecurringQuote\RecurringQuoteWasUpdated; use App\Events\RecurringQuote\RecurringQuoteWasUpdated;
use App\Listeners\Account\StripeConnectFailureListener;
use App\Listeners\Activity\CreatedSubscriptionActivity; use App\Listeners\Activity\CreatedSubscriptionActivity;
use App\Listeners\Activity\SubscriptionDeletedActivity; use App\Listeners\Activity\SubscriptionDeletedActivity;
use App\Listeners\Activity\SubscriptionUpdatedActivity; use App\Listeners\Activity\SubscriptionUpdatedActivity;
@ -234,6 +239,7 @@ use App\Events\RecurringQuote\RecurringQuoteWasRestored;
use App\Listeners\Activity\SubscriptionArchivedActivity; use App\Listeners\Activity\SubscriptionArchivedActivity;
use App\Listeners\Activity\SubscriptionRestoredActivity; use App\Listeners\Activity\SubscriptionRestoredActivity;
use App\Listeners\Invoice\InvoiceFailedEmailNotification; use App\Listeners\Invoice\InvoiceFailedEmailNotification;
use SocialiteProviders\Microsoft\MicrosoftExtendSocialite;
use App\Events\RecurringExpense\RecurringExpenseWasCreated; use App\Events\RecurringExpense\RecurringExpenseWasCreated;
use App\Events\RecurringExpense\RecurringExpenseWasDeleted; use App\Events\RecurringExpense\RecurringExpenseWasDeleted;
use App\Events\RecurringExpense\RecurringExpenseWasUpdated; use App\Events\RecurringExpense\RecurringExpenseWasUpdated;
@ -587,6 +593,9 @@ class EventServiceProvider extends ServiceProvider
TaskWasRestored::class => [ TaskWasRestored::class => [
TaskRestoredActivity::class, TaskRestoredActivity::class,
], ],
StripeConnectFailure::class => [
StripeConnectFailureListener::class,
],
SubscriptionWasCreated::class => [ SubscriptionWasCreated::class => [
CreatedSubscriptionActivity::class, CreatedSubscriptionActivity::class,
], ],

View File

@ -136,7 +136,11 @@ class InvoiceMigrationRepository extends BaseRepository
$state['finished_amount'] = $model->amount; $state['finished_amount'] = $model->amount;
$model = $model->service()->applyNumber()->setReminder()->save(); $model = $model->service()->applyNumber()->save();
if ($class->name == Invoice::class) {
$model->service()->setReminder()->save();
}
if ($class->name == Invoice::class || $class->name == RecurringInvoice::class) { if ($class->name == Invoice::class || $class->name == RecurringInvoice::class) {
if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) { if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) {

View File

@ -23,11 +23,11 @@ use horstoeko\zugferd\codelists\ZugferdDutyTaxFeeCategories;
class ZugferdEInvoice extends AbstractService class ZugferdEInvoice extends AbstractService
{ {
public function __construct(public Invoice $invoice, private array $tax_map = []) public function __construct(public Invoice $invoice, private readonly bool $returnObject = false, private array $tax_map = [])
{ {
} }
public function run() public function run(): string|ZugferdDocumentBuilder
{ {
$company = $this->invoice->company; $company = $this->invoice->company;
@ -175,7 +175,9 @@ class ZugferdEInvoice extends AbstractService
$xrechnung->writeFile(Storage::disk($disk)->path($client->e_invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName("xml"))); $xrechnung->writeFile(Storage::disk($disk)->path($client->e_invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName("xml")));
// The validity can be checked using https://portal3.gefeg.com/invoice/validation or https://e-rechnung.bayern.de/app/#/upload // The validity can be checked using https://portal3.gefeg.com/invoice/validation or https://e-rechnung.bayern.de/app/#/upload
if ($this->returnObject){
return $xrechnung;
}
return $client->e_invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName("xml"); return $client->e_invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName("xml");
} }

View File

@ -43,9 +43,10 @@ class GetInvoiceEInvoice extends AbstractService
$file = Storage::disk($disk)->exists($file_path); $file = Storage::disk($disk)->exists($file_path);
if (! $file) { if (! $file) {
$file_path = (new CreateEInvoice($this->invoice, false))->handle(); $file_path = (new CreateEInvoice($this->invoice))->handle();
} (new \App\Jobs\Invoice\MergeEInvoice($this->invoice))->handle();
}
return $file_path; return $file_path;
} }
} }

View File

@ -12,6 +12,8 @@
namespace App\Services\Invoice; namespace App\Services\Invoice;
use App\Jobs\Entity\CreateEntityPdf; use App\Jobs\Entity\CreateEntityPdf;
use App\Jobs\Invoice\CreateEInvoice;
use App\Jobs\Invoice\MergeEInvoice;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Invoice; use App\Models\Invoice;
use App\Services\AbstractService; use App\Services\AbstractService;
@ -47,7 +49,10 @@ class GetInvoicePdf extends AbstractService
if (! $file) { if (! $file) {
$file_path = (new CreateEntityPdf($invitation))->handle(); $file_path = (new CreateEntityPdf($invitation))->handle();
} }
if ($this->invoice->client->getSetting('enable_e_invoice')){
(new CreateEInvoice($this->invoice))->handle();
(new MergeEInvoice($this->invoice, $file_path))->handle();
}
return $file_path; return $file_path;
} }
} }

View File

@ -343,6 +343,7 @@ class InvoiceService
})->toArray(); })->toArray();
$this->deletePdf(); $this->deletePdf();
$this->deleteEInvoice();
return $this; return $this;
} }
@ -410,6 +411,7 @@ class InvoiceService
$this->invoice = $this->invoice->calc()->getInvoice(); $this->invoice = $this->invoice->calc()->getInvoice();
$this->deletePdf(); $this->deletePdf();
$this->deleteEInvoice();
/* 24-03-2022 */ /* 24-03-2022 */
$new_balance = $this->invoice->balance; $new_balance = $this->invoice->balance;
@ -465,7 +467,8 @@ class InvoiceService
if ($invitation->invoice->client->getSetting('enable_e_invoice') && $invitation instanceof InvoiceInvitation) if ($invitation->invoice->client->getSetting('enable_e_invoice') && $invitation instanceof InvoiceInvitation)
{ {
(new CreateEInvoice($invitation->invoice, true))->handle(); (new CreateEInvoice($invitation->invoice))->handle();
(new MergeEInvoice($invitation->invoice))->run();
} }
}); });
@ -478,7 +481,7 @@ class InvoiceService
CreateEntityPdf::dispatch($invitation); CreateEntityPdf::dispatch($invitation);
if ($invitation->invoice->client->getSetting('enable_e_invoice') && $invitation instanceof InvoiceInvitation) { if ($invitation->invoice->client->getSetting('enable_e_invoice') && $invitation instanceof InvoiceInvitation) {
CreateEInvoice::dispatch($invitation->invoice, true); CreateEInvoice::dispatch($invitation->invoice);
} }
}); });

View File

@ -4,58 +4,26 @@ namespace App\Services\Invoice;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Invoice; use App\Models\Invoice;
use App\Services\AbstractService;
use horstoeko\zugferd\ZugferdDocumentPdfBuilder;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use horstoeko\zugferd\ZugferdDocumentReader;
class MergeEInvoice extends AbstractService class MergeEInvoice
{ {
public function __construct(public Invoice $invoice, public ?ClientContact $contact = null)
/**
* @param Invoice $invoice
*/
public function __construct(public Invoice $invoice, public string $pdf_path = "")
{ {
} }
/**
* @throws \Exception
*/
public function run(): void public function run(): void
{ {
$e_invoice_type = $this->invoice->client->getSetting('e_invoice_type'); if (!empty($this->pdf_path)) {
switch ($e_invoice_type) { (new \App\Jobs\Invoice\MergeEInvoice($this->invoice, $this->pdf_path))->handle();
case "EN16931":
case "XInvoice_2_2":
case "XInvoice_2_1":
case "XInvoice_2_0":
case "XInvoice_1_0":
case "XInvoice-Extended":
case "XInvoice-BasicWL":
case "XInvoice-Basic":
$this->embedEInvoiceZuGFerD();
//case "Facturae_3.2":
//case "Facturae_3.2.1":
//case "Facturae_3.2.2":
//
default:
$this->embedEInvoiceZuGFerD();
break;
} }
else {
(new \App\Jobs\Invoice\MergeEInvoice($this->invoice))->handle();
} }
/**
* @throws \Exception
*/
private function embedEInvoiceZuGFerD(): void
{
$filepath_pdf = $this->invoice->client->invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName();
$e_invoice_path = $this->invoice->client->e_invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName("xml");
$document = ZugferdDocumentReader::readAndGuessFromFile($e_invoice_path);
$disk = config('filesystems.default');
if (!Storage::disk($disk)->exists($this->invoice->client->e_invoice_filepath($this->invoice->invitations->first()))) {
Storage::makeDirectory($this->invoice->client->e_invoice_filepath($this->invoice->invitations->first()));
}
$pdfBuilder = new ZugferdDocumentPdfBuilder($document, Storage::disk($disk)->path($filepath_pdf));
$pdfBuilder->generateDocument();
$pdfBuilder->saveDocument(Storage::disk($disk)->path($filepath_pdf));
} }
} }

View File

@ -57,7 +57,7 @@ class PurchaseOrderExpense
$expense->number = empty($expense->number) ? $this->getNextExpenseNumber($expense) : $expense->number; $expense->number = empty($expense->number) ? $this->getNextExpenseNumber($expense) : $expense->number;
$expense->save(); $expense->saveQuietly();
event('eloquent.created: App\Models\Expense', $expense); event('eloquent.created: App\Models\Expense', $expense);
$this->purchase_order->expense_id = $expense->id; $this->purchase_order->expense_id = $expense->id;

View File

@ -20,11 +20,8 @@ class PurchaseOrderService
{ {
use MakesHash; use MakesHash;
public PurchaseOrder $purchase_order; public function __construct(public PurchaseOrder $purchase_order)
public function __construct(PurchaseOrder $purchase_order)
{ {
$this->purchase_order = $purchase_order;
} }
public function createInvitations() public function createInvitations()
@ -156,7 +153,7 @@ class PurchaseOrderService
/** /**
* Saves the purchase order. * Saves the purchase order.
* @return \App\Models\PurchaseOrder object * @return \App\Models\PurchaseOrder
*/ */
public function save(): ?PurchaseOrder public function save(): ?PurchaseOrder
{ {

View File

@ -52,8 +52,8 @@ class TemplateEngine
public $template; public $template;
/** @var \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder | \App\Models\RecurringInvoice $entity_obj **/ /** @var \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder | \App\Models\RecurringInvoice | \App\Models\Payment $entity_obj **/
private \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder | \App\Models\RecurringInvoice $entity_obj; private \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder | \App\Models\RecurringInvoice | \App\Models\Payment $entity_obj;
/** @var \App\Models\Company | \App\Models\Client | null $settings_entity **/ /** @var \App\Models\Company | \App\Models\Client | null $settings_entity **/
private $settings_entity; private $settings_entity;

743
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@ return [
'batch' => true, 'batch' => true,
'cache_connection' => 'sentinel-cache', 'cache_connection' => 'sentinel-cache',
// 'cache_connection' => 'cache',
/* /*
* The default key used to store * The default key used to store
* metrics for batching * metrics for batching

View File

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

View File

@ -5143,6 +5143,7 @@ $LANG = array(
'is_tax_exempt' => 'Tax Exempt', 'is_tax_exempt' => 'Tax Exempt',
'drop_files_here' => 'Drop files here', 'drop_files_here' => 'Drop files here',
'upload_files' => 'Upload Files', 'upload_files' => 'Upload Files',
'download_e_invoice' => 'Download E-Invoice',
'triangular_tax_info' => 'Intra-community triangular transaction', 'triangular_tax_info' => 'Intra-community triangular transaction',
'intracommunity_tax_info' => 'Tax-free intra-community delivery', 'intracommunity_tax_info' => 'Tax-free intra-community delivery',
'reverse_tax_info' => 'Please note that this supply is subject to reverse charge', 'reverse_tax_info' => 'Please note that this supply is subject to reverse charge',

View File

@ -8,6 +8,7 @@ parameters:
excludePaths: excludePaths:
- 'vendor/' - 'vendor/'
- 'app/Jobs/Ninja/*' - 'app/Jobs/Ninja/*'
- 'app/Models/Presenters/*'
- 'app/Console/Commands/*' - 'app/Console/Commands/*'
- 'app/DataMapper/Analytics/*' - 'app/DataMapper/Analytics/*'
- 'app/PaymentDrivers/Authorize/*' - 'app/PaymentDrivers/Authorize/*'
@ -17,10 +18,7 @@ parameters:
- App\DataMapper\FeesAndLimits - App\DataMapper\FeesAndLimits
reportUnmatchedIgnoredErrors: false reportUnmatchedIgnoredErrors: false
ignoreErrors: ignoreErrors:
- '#Call to an undefined method [a-zA-Z0-9\\_]+::company\(\)#'
- '#Call to an undefined method [a-zA-Z0-9\\_]+::entityFilter\(\)#'
- '#Call to an undefined method [a-zA-Z0-9\\_]+::exclude\(\)#'
- '#Array has 2 duplicate keys with value#' - '#Array has 2 duplicate keys with value#'
- '#Undefined method#' - '#Call to an undefined method#'
- '#makeHidden#' - '#makeHidden#'
- '#Socialite#' - '#Socialite#'

View File

@ -0,0 +1,59 @@
@component('email.template.admin', ['design' => 'light', 'settings' => $settings, 'logo' => $logo])
<div class="center">
@isset($greeting)
<p>{{ $greeting }}</p>
@endisset
@isset($title)
<h1>{{ $title }}</h1>
@endisset
@isset($h2)
<h2>{{ $title }}</h2>
@endisset
<div style="margin-top: 10px; margin-bottom: 30px;">
@isset($body)
{!! $body !!}
@endisset
@isset($slot)
{{ $slot }}
@endisset
</div>
@isset($additional_info)
<p>{{ $additional_info }}</p>
@endisset
@isset($url)
<!--[if (gte mso 9)|(IE)]>
<table align="center" cellspacing="0" cellpadding="0" style="width: 600px;">
<tr>
<td align="center" valign="top">
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" >
<tbody><tr>
<td align="center" class="new_button" style="border-radius: 2px; background-color: {{ $settings->primary_color }} ;">
<a href="{{ $url }}" target="_blank" class="new_button" style="text-decoration: none; border: 1px solid {{ $settings->primary_color }}; display: inline-block; border-radius: 2px; padding-top: 15px; padding-bottom: 15px; padding-left: 25px; padding-right: 25px; font-size: 20px; color: #fff">
<singleline label="cta button">{{ ctrans($button) }}</singleline>
</a>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
@endisset
@isset($signature)
<p>{!! nl2br($signature) !!}</p>
@endisset
</div>
@endcomponent

View File

@ -0,0 +1,5 @@
{!! $title !!}
{!! $text_body !!}
{!! $url !!}

View File

@ -9,6 +9,15 @@
</svg> </svg>
</div> </div>
</button> </button>
<button wire:loading.attr="disabled" wire:click="downloadEInvoice" class="bg-primary text-white px-4 py-4 lg:px-2 lg:py-2 rounded" type="button">
<span>{{ ctrans('texts.download_e_invoice') }}</span>
<div wire:loading wire:target="downloadEInvoice">
<svg class="animate-spin h-5 w-5 text-blue" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
</button>
</div> </div>
<div class="hidden lg:block"> <div class="hidden lg:block">
<div wire:init="getPdf()"> <div wire:init="getPdf()">

View File

@ -16,7 +16,8 @@
<form action="{{ route('client.invoices.bulk') }}" method="post" id="bulkActions"> <form action="{{ route('client.invoices.bulk') }}" method="post" id="bulkActions">
@csrf @csrf
<button type="submit" onclick="setTimeout(() => this.disabled = true, 0); setTimeout(() => this.disabled = false, 5000); return true;" class="button button-primary bg-primary" name="action" value="download">{{ ctrans('texts.download') }}</button> <button type="submit" onclick="setTimeout(() => this.disabled = true, 0); setTimeout(() => this.disabled = false, 5000); return true;" class="button button-primary bg-primary" name="action" value="download">{{ ctrans('texts.download') }}</button>
@csrf
<button type="submit" onclick="setTimeout(() => this.disabled = true, 0); setTimeout(() => this.disabled = false, 5000); return true;" class="button button-primary bg-secondary" name="action-xml" value="download">{{ ctrans('texts.download_xml') }}</button>
@if(!empty(auth()->user()->client->service()->getPaymentMethods(0))) @if(!empty(auth()->user()->client->service()->getPaymentMethods(0)))
<button onclick="setTimeout(() => this.disabled = true, 0); return true;" type="submit" class="button button-primary bg-primary" name="action" value="payment">{{ ctrans('texts.pay_now') }}</button> <button onclick="setTimeout(() => this.disabled = true, 0); return true;" type="submit" class="button button-primary bg-primary" name="action" value="payment">{{ ctrans('texts.pay_now') }}</button>
@endif @endif

View File

@ -42,9 +42,10 @@ class EInvoiceTest extends TestCase
$this->company->e_invoice_type = "EN16931"; $this->company->e_invoice_type = "EN16931";
$this->invoice->client->routing_id = 'DE123456789'; $this->invoice->client->routing_id = 'DE123456789';
$this->invoice->client->save(); $this->invoice->client->save();
$xinvoice = (new CreateEInvoice($this->invoice, false))->handle(); $e_invoice = (new CreateEInvoice($this->invoice))->handle();
$this->assertNotNull($xinvoice); (new \App\Jobs\Invoice\MergeEInvoice($this->invoice))->handle();
$this->assertTrue(Storage::exists($xinvoice)); $this->assertNotNull($e_invoice);
$this->assertTrue(Storage::exists($e_invoice));
} }
/** /**
@ -56,9 +57,8 @@ class EInvoiceTest extends TestCase
$this->invoice->client->routing_id = 'DE123456789'; $this->invoice->client->routing_id = 'DE123456789';
$this->invoice->client->save(); $this->invoice->client->save();
$xinvoice = (new CreateEInvoice($this->invoice, false))->handle(); $e_invoice = (new CreateEInvoice($this->invoice))->handle();
nlog(Storage::path($xinvoice)); $document = ZugferdDocumentReader::readAndGuessFromFile(Storage::path($e_invoice));
$document = ZugferdDocumentReader::readAndGuessFromFile(Storage::path($xinvoice));
$document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $documentcurrency, $taxcurrency, $taxname, $documentlangeuage, $rest); $document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $documentcurrency, $taxcurrency, $taxname, $documentlangeuage, $rest);
$this->assertEquals($this->invoice->number, $documentno); $this->assertEquals($this->invoice->number, $documentno);
} }
@ -69,7 +69,8 @@ class EInvoiceTest extends TestCase
public function checkEmbededPDFFile() public function checkEmbededPDFFile()
{ {
$pdf = (new CreateEntityPdf($this->invoice->invitations()->first()))->handle(); $pdf = (new CreateEntityPdf($this->invoice->invitations()->first()))->handle();
(new CreateEInvoice($this->invoice, true, $pdf))->handle(); (new CreateEInvoice($this->invoice))->handle();
(new \App\Jobs\Invoice\MergeEInvoice($this->invoice))->handle();
$document = ZugferdDocumentReader::readAndGuessFromFile($pdf); $document = ZugferdDocumentReader::readAndGuessFromFile($pdf);
$document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $documentcurrency, $taxcurrency, $taxname, $documentlangeuage, $rest); $document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $documentcurrency, $taxcurrency, $taxname, $documentlangeuage, $rest);
$this->assertEquals($this->invoice->number, $documentno); $this->assertEquals($this->invoice->number, $documentno);