1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-12 22:22:32 +01:00

Merge pull request #7598 from turbo124/v5-develop

Fixes for Purchase Orders
This commit is contained in:
David Bomba 2022-06-30 18:25:13 +10:00 committed by GitHub
commit 0b717aceac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1922 additions and 217 deletions

View File

@ -25,6 +25,9 @@ class CompanySettings extends BaseSettings
/*Invoice*/
public $auto_archive_invoice = false; // @implemented
public $qr_iban = ''; //@implemented
public $besr_id = ''; //@implemented
public $lock_invoices = 'off'; //off,when_sent,when_paid //@implemented
public $enable_client_portal_tasks = false; //@ben to implement
@ -289,6 +292,8 @@ class CompanySettings extends BaseSettings
public $auto_archive_invoice_cancelled = false;
public static $casts = [
'besr_id' => 'string',
'qr_iban' => 'string',
'email_subject_purchase_order' => 'string',
'email_template_purchase_order' => 'string',
'require_purchase_order_signature' => 'bool',

View File

@ -11,6 +11,7 @@
namespace App\Factory;
use App\Models\CompanyUser;
use App\Models\User;
class UserFactory

View File

@ -0,0 +1,146 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\SwissQr;
use App\Models\Client;
use App\Models\Company;
use App\Models\Invoice;
use Sprain\SwissQrBill as QrBill;
/**
* SwissQrGenerator.
*/
class SwissQrGenerator
{
protected Company $company;
protected Invoice $invoice;
protected Client $client;
public function __construct(Invoice $invoice, Company $company)
{
$this->company = $company;
$this->invoice = $invoice;
$this->client = $invoice->client;
}
private function calcDueAmount()
{
if($this->invoice->partial > 0)
return $this->invoice->partial;
if($this->invoice->status_id == Invoice::STATUS_DRAFT)
return $this->invoice->amount;
return $this->invoice->balance;
}
public function run()
{
// This is an example how to create a typical qr bill:
// - with reference number
// - with known debtor
// - with specified amount
// - with human-readable additional information
// - using your QR-IBAN
//
// Likely the most common use-case in the business world.
// Create a new instance of QrBill, containing default headers with fixed values
$qrBill = QrBill\QrBill::create();
// Add creditor information
// Who will receive the payment and to which bank account?
$qrBill->setCreditor(
QrBill\DataGroup\Element\CombinedAddress::create(
$this->company->present()->name(),
$this->company->present()->address1(),
$this->company->present()->getCompanyCityState(),
'CH'
));
$qrBill->setCreditorInformation(
QrBill\DataGroup\Element\CreditorInformation::create(
$this->company->present()->qr_iban() ?: '' // This is a special QR-IBAN. Classic IBANs will not be valid here.
));
// Add debtor information
// Who has to pay the invoice? This part is optional.
//
// Notice how you can use two different styles of addresses: CombinedAddress or StructuredAddress
// They are interchangeable for creditor as well as debtor.
$qrBill->setUltimateDebtor(
QrBill\DataGroup\Element\StructuredAddress::createWithStreet(
$this->client->present()->name(),
$this->client->address1 ?: '',
$this->client->address2 ?: '',
$this->client->postal_code ?: '',
$this->client->city ?: '',
'CH'
));
// Add payment amount information
// What amount is to be paid?
$qrBill->setPaymentAmountInformation(
QrBill\DataGroup\Element\PaymentAmountInformation::create(
'CHF',
$this->calcDueAmount()
));
// Add payment reference
// This is what you will need to identify incoming payments.
$referenceNumber = QrBill\Reference\QrPaymentReferenceGenerator::generate(
$this->company->present()->besr_id() ?: '', // You receive this number from your bank (BESR-ID). Unless your bank is PostFinance, in that case use NULL.
$this->invoice->number // A number to match the payment with your internal data, e.g. an invoice number
);
$qrBill->setPaymentReference(
QrBill\DataGroup\Element\PaymentReference::create(
QrBill\DataGroup\Element\PaymentReference::TYPE_QR,
$referenceNumber
));
// Optionally, add some human-readable information about what the bill is for.
$qrBill->setAdditionalInformation(
QrBill\DataGroup\Element\AdditionalInformation::create(
$this->invoice->public_notes ?: ''
)
);
// Now get the QR code image and save it as a file.
try {
// $qrBill->getQrCode()->writeFile(__DIR__ . '/qr.png');
// $qrBill->getQrCode()->writeFile(__DIR__ . '/qr.svg');
} catch (\Exception $e) {
foreach($qrBill->getViolations() as $key => $violation) {
}
// return $e->getMessage();
}
$output = new QrBill\PaymentPart\Output\HtmlOutput\HtmlOutput($qrBill, 'en');
$html = $output
->setPrintable(false)
->getPaymentPart();
return $html;
}
}

View File

@ -0,0 +1,470 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers;
use App\DataMapper\Analytics\LivePreview;
use App\Factory\CreditFactory;
use App\Factory\InvoiceFactory;
use App\Factory\PurchaseOrderFactory;
use App\Factory\QuoteFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Preview\PreviewInvoiceRequest;
use App\Http\Requests\Preview\PreviewPurchaseOrderRequest;
use App\Jobs\Util\PreviewPdf;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Repositories\CreditRepository;
use App\Repositories\InvoiceRepository;
use App\Repositories\PurchaseOrderRepository;
use App\Repositories\QuoteRepository;
use App\Repositories\RecurringInvoiceRepository;
use App\Services\PdfMaker\Design as PdfDesignModel;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\Design;
use App\Services\PdfMaker\PdfMaker;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\Pdf\PageNumbering;
use App\Utils\VendorHtmlEngine;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Response;
use Turbo124\Beacon\Facades\LightLogs;
class PreviewPurchaseOrderController extends BaseController
{
use MakesHash;
use MakesInvoiceHtml;
use PageNumbering;
public function __construct()
{
parent::__construct();
}
/**
* Returns a template filled with entity variables.
*
* @return \Illuminate\Http\Response
*
* @OA\Post(
* path="/api/v1/preview/purchase_order",
* operationId="getPreviewPurchaseOrder",
* tags={"preview"},
* summary="Returns a pdf preview for purchase order",
* description="Returns a pdf preview for purchase order.",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Response(
* response=200,
* description="The pdf 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 show()
{
if (request()->has('entity') &&
request()->has('entity_id') &&
! empty(request()->input('entity')) &&
! empty(request()->input('entity_id')) &&
request()->has('body')) {
$design_object = json_decode(json_encode(request()->input('design')));
if (! is_object($design_object)) {
return response()->json(['message' => ctrans('texts.invalid_design_object')], 400);
}
$entity_obj = PurchaseOrder::whereId($this->decodePrimaryKey(request()->input('entity_id')))->company()->first();
if (! $entity_obj) {
return $this->blankEntity();
}
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($entity_obj->company->locale());
$t->replace(Ninja::transformTranslations($entity_obj->company->settings));
$html = new VendorHtmlEngine($entity_obj->invitations()->first());
$design_namespace = 'App\Services\PdfMaker\Designs\\'.request()->design['name'];
$design_class = new $design_namespace();
$state = [
'template' => $design_class->elements([
'client' => null,
'vendor' => $entity_obj->vendor,
'entity' => $entity_obj,
'pdf_variables' => (array) $entity_obj->company->settings->pdf_variables,
'variables' => $html->generateLabelsAndValues(),
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $entity_obj->company->markdown_enabled,
];
$design = new Design(request()->design['name']);
$maker = new PdfMaker($state);
$maker
->design($design)
->build();
if (request()->query('html') == 'true') {
return $maker->getCompiledHTML();
}
//if phantom js...... inject here..
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company());
if($numbered_pdf)
$pdf = $numbered_pdf;
return $pdf;
}
//else
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), auth()->user()->company());
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
return $this->blankEntity();
}
public function live(PreviewPurchaseOrderRequest $request)
{
$company = auth()->user()->company();
MultiDB::setDb($company->db);
$repo = new PurchaseOrderRepository();
$entity_obj = PurchaseOrderFactory::create($company->id, auth()->user()->id);
$class = PurchaseOrder::class;
try {
DB::connection(config('database.default'))->beginTransaction();
if($request->has('entity_id')){
$entity_obj = $class::on(config('database.default'))
->with('vendor.company')
->where('id', $this->decodePrimaryKey($request->input('entity_id')))
->where('company_id', $company->id)
->withTrashed()
->first();
}
$entity_obj = $repo->save($request->all(), $entity_obj);
if(!$request->has('entity_id'))
$entity_obj->service()->fillDefaults()->save();
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($entity_obj->company->locale());
$t->replace(Ninja::transformTranslations($entity_obj->company->settings));
$html = new VendorHtmlEngine($entity_obj->invitations()->first());
$design = \App\Models\Design::find($entity_obj->design_id);
/* Catch all in case migration doesn't pass back a valid design */
if(!$design)
$design = \App\Models\Design::find(2);
if ($design->is_custom) {
$options = [
'custom_partials' => json_decode(json_encode($design->design), true)
];
$template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options);
} else {
$template = new PdfMakerDesign(strtolower($design->name));
}
$variables = $html->generateLabelsAndValues();
$state = [
'template' => $template->elements([
'client' => null,
'vendor' => $entity_obj->vendor,
'entity' => $entity_obj,
'pdf_variables' => (array) $entity_obj->company->settings->pdf_variables,
'variables' => $html->generateLabelsAndValues(),
'$product' => $design->design->product,
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $entity_obj->company->markdown_enabled,
];
$maker = new PdfMaker($state);
$maker
->design($template)
->build();
DB::connection(config('database.default'))->rollBack();
if (request()->query('html') == 'true') {
return $maker->getCompiledHTML();
}
}
catch(\Exception $e){
DB::connection(config('database.default'))->rollBack();
return;
}
//if phantom js...... inject here..
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company());
if($numbered_pdf)
$pdf = $numbered_pdf;
return $pdf;
}
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), $company);
if(Ninja::isHosted())
{
LightLogs::create(new LivePreview())
->increment()
->queue();
}
$response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf');
return $response;
}
private function blankEntity()
{
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations(auth()->user()->company()->settings));
$invitation = PurchaseOrderInvitation::where('company_id', auth()->user()->company()->id)->orderBy('id', 'desc')->first();
/* If we don't have a valid invitation in the system - create a mock using transactions */
if(!$invitation)
return $this->mockEntity();
$design_object = json_decode(json_encode(request()->input('design')));
if (! is_object($design_object)) {
return response()->json(['message' => 'Invalid custom design object'], 400);
}
$html = new VendorHtmlEngine($invitation);
$design = new Design(Design::CUSTOM, ['custom_partials' => request()->design['design']]);
$state = [
'template' => $design->elements([
'client' => null,
'vendor' => $invitation->purchase_order->vendor,
'entity' => $invitation->purchase_order,
'pdf_variables' => (array) $invitation->company->settings->pdf_variables,
'products' => request()->design['design']['product'],
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $invitation->company->markdown_enabled,
];
$maker = new PdfMaker($state);
$maker
->design($design)
->build();
if (request()->query('html') == 'true') {
return $maker->getCompiledHTML();
}
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company());
if($numbered_pdf)
$pdf = $numbered_pdf;
return $pdf;
}
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), auth()->user()->company());
$response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf');
return $response;
}
private function mockEntity()
{
DB::connection(auth()->user()->company()->db)->beginTransaction();
$vendor = Vendor::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
]);
$contact = VendorContact::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
'vendor_id' => $vendor->id,
'is_primary' => 1,
'send_email' => true,
]);
$purchase_order = PurchaseOrder::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
'vendor_id' => $vendor->id,
'terms' => 'Sample Terms',
'footer' => 'Sample Footer',
'public_notes' => 'Sample Public Notes',
]);
$invitation = PurchaseOrderInvitation::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
'purchase_order_id' => $purchase_order->id,
'vendor_contact_id' => $contact->id,
]);
$purchase_order->setRelation('invitations', $invitation);
$purchase_order->setRelation('vendor', $vendor);
$purchase_order->setRelation('company', auth()->user()->company());
$purchase_order->load('vendor.company');
$design_object = json_decode(json_encode(request()->input('design')));
if (! is_object($design_object)) {
return response()->json(['message' => 'Invalid custom design object'], 400);
}
$html = new VendorHtmlEngine($purchase_order->invitations()->first());
$design = new Design(Design::CUSTOM, ['custom_partials' => request()->design['design']]);
$state = [
'template' => $design->elements([
'client' => null,
'vendor' => $purchase_order->vendor,
'entity' => $purchase_order,
'pdf_variables' => (array) $purchase_order->company->settings->pdf_variables,
'products' => request()->design['design']['product'],
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $purchase_order->company->markdown_enabled,
];
$maker = new PdfMaker($state);
$maker
->design($design)
->build();
DB::connection(auth()->user()->company()->db)->rollBack();
if (request()->query('html') == 'true') {
return $maker->getCompiledHTML();
}
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company());
if($numbered_pdf)
$pdf = $numbered_pdf;
return $pdf;
}
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), auth()->user()->company());
$response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf');
return $response;
}
}

View File

@ -17,11 +17,13 @@ use App\Events\Misc\InvitationWasViewed;
use App\Events\Quote\QuoteWasViewed;
use App\Http\Controllers\Controller;
use App\Jobs\Entity\CreateRawPdf;
use App\Jobs\Vendor\CreatePurchaseOrderPdf;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Models\QuoteInvitation;
use App\Services\ClientPortal\InstantPayment;
@ -95,50 +97,32 @@ class InvitationController extends Controller
}
public function download(string $invitation_key)
{
$invitation = PurchaseOrderInvitation::withTrashed()
->where('key', $invitation_key)
->with('contact.vendor')
->firstOrFail();
if(!$invitation)
return response()->json(["message" => "no record found"], 400);
// public function routerForDownload(string $entity, string $invitation_key)
// {
$file_name = $invitation->purchase_order->numberFormatter().'.pdf';
// set_time_limit(45);
// $file = CreateRawPdf::dispatchNow($invitation, $invitation->company->db);
// if(Ninja::isHosted())
// return $this->returnRawPdf($entity, $invitation_key);
$file = (new CreatePurchaseOrderPdf($invitation))->rawPdf();
// return redirect('client/'.$entity.'/'.$invitation_key.'/download_pdf');
// }
$headers = ['Content-Type' => 'application/pdf'];
// private function returnRawPdf(string $entity, string $invitation_key)
// {
if(request()->input('inline') == 'true')
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
// if(!in_array($entity, ['invoice', 'credit', 'quote', 'recurring_invoice']))
// return response()->json(['message' => 'Invalid resource request']);
return response()->streamDownload(function () use($file) {
echo $file;
}, $file_name, $headers);
}
// $key = $entity.'_id';
// $entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
// $invitation = $entity_obj::where('key', $invitation_key)
// ->with('contact.client')
// ->firstOrFail();
// if(!$invitation)
// return response()->json(["message" => "no record found"], 400);
// $file_name = $invitation->purchase_order->numberFormatter().'.pdf';
// $file = CreateRawPdf::dispatchNow($invitation, $invitation->company->db);
// $headers = ['Content-Type' => 'application/pdf'];
// if(request()->input('inline') == 'true')
// $headers = array_merge($headers, ['Content-Disposition' => 'inline']);
// return response()->streamDownload(function () use($file) {
// echo $file;
// }, $file_name, $headers);
// }

View File

@ -0,0 +1,62 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Preview;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Project\ValidProjectForClient;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class PreviewPurchaseOrderRequest extends Request
{
use MakesHash;
use CleanLineItems;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('create', PurchaseOrder::class);
}
public function rules()
{
$rules = [];
$rules['number'] = ['nullable'];
return $rules;
}
protected function prepareForValidation()
{
$input = $this->all();
$input = $this->decodePrimaryKeys($input);
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
$input['amount'] = 0;
$input['balance'] = 0;
$input['number'] = ctrans('texts.live_preview') . " #". rand(0,1000);
$this->replace($input);
}
}

View File

@ -1423,21 +1423,21 @@ class CompanyImport implements ShouldQueue
$new_obj->company_id = $this->company->id;
$new_obj->fill($obj_array);
$new_obj->save(['timestamps' => false]);
$new_obj->number = $this->getNextInvoiceNumber($client = Client::find($obj_array['client_id']),$new_obj);
$new_obj->number = $this->getNextInvoiceNumber($client = Client::withTrashed()->find($obj_array['client_id']),$new_obj);
}
elseif($class == 'App\Models\Payment' && is_null($obj->{$match_key})){
$new_obj = new Payment();
$new_obj->company_id = $this->company->id;
$new_obj->fill($obj_array);
$new_obj->save(['timestamps' => false]);
$new_obj->number = $this->getNextPaymentNumber($client = Client::find($obj_array['client_id']), $new_obj);
$new_obj->number = $this->getNextPaymentNumber($client = Client::withTrashed()->find($obj_array['client_id']), $new_obj);
}
elseif($class == 'App\Models\Quote' && is_null($obj->{$match_key})){
$new_obj = new Quote();
$new_obj->company_id = $this->company->id;
$new_obj->fill($obj_array);
$new_obj->save(['timestamps' => false]);
$new_obj->number = $this->getNextQuoteNumber($client = Client::find($obj_array['client_id']), $new_obj);
$new_obj->number = $this->getNextQuoteNumber($client = Client::withTrashed()->find($obj_array['client_id']), $new_obj);
}
elseif($class == 'App\Models\ClientContact'){
$new_obj = new ClientContact();

View File

@ -308,8 +308,9 @@ class NinjaMailerJob implements ShouldQueue
private function preFlightChecksFail()
{
/* If we are migrating data we don't want to fire any emails */
if ($this->nmo->company->is_disabled && !$this->override)
if($this->nmo->company->is_disabled && !$this->override)
return true;
/* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */
@ -324,6 +325,9 @@ class NinjaMailerJob implements ShouldQueue
if(Ninja::isHosted() && $this->company->account && $this->company->account->emailQuotaExceeded())
return true;
if(Ninja::isHosted() && $this->company->account && $this->nmo->company->account->is_flagged)
return true;
/* Ensure the user has a valid email address */
if(!str_contains($this->nmo->to_user->email, "@"))
return true;

View File

@ -65,6 +65,10 @@ class CreatePurchaseOrderPdf implements ShouldQueue
public $vendor;
private string $path = '';
private string $file_path = '';
/**
* Create a new job instance.
*
@ -88,6 +92,32 @@ class CreatePurchaseOrderPdf implements ShouldQueue
}
public function handle()
{
$pdf = $this->rawPdf();
if ($pdf) {
try{
if(!Storage::disk($this->disk)->exists($this->path))
Storage::disk($this->disk)->makeDirectory($this->path, 0775);
Storage::disk($this->disk)->put($this->file_path, $pdf, 'public');
}
catch(\Exception $e)
{
throw new FilePermissionsFailure($e->getMessage());
}
}
return $this->file_path;
}
public function rawPdf()
{
MultiDB::setDb($this->company->db);
@ -109,10 +139,10 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$entity_design_id = '';
$path = $this->vendor->purchase_order_filepath($this->invitation);
$this->path = $this->vendor->purchase_order_filepath($this->invitation);
$entity_design_id = 'purchase_order_design_id';
$file_path = $path.$this->entity->numberFormatter().'.pdf';
$this->file_path = $this->path.$this->entity->numberFormatter().'.pdf';
$entity_design_id = $this->entity->design_id ? $this->entity->design_id : $this->decodePrimaryKey('Wpmbk5ezJn');
@ -191,25 +221,8 @@ class CreatePurchaseOrderPdf implements ShouldQueue
info($maker->getCompiledHTML());
}
if ($pdf) {
return $pdf;
try{
if(!Storage::disk($this->disk)->exists($path))
Storage::disk($this->disk)->makeDirectory($path, 0775);
Storage::disk($this->disk)->put($file_path, $pdf, 'public');
}
catch(\Exception $e)
{
throw new FilePermissionsFailure($e->getMessage());
}
}
return $file_path;
}
public function failed($e)

View File

@ -33,7 +33,7 @@ class Account extends BaseModel
use PresentableTrait;
use MakesHash;
private $free_plan_email_quota = 250;
private $free_plan_email_quota = 100;
private $paid_plan_email_quota = 500;
/**
@ -373,10 +373,15 @@ class Account extends BaseModel
public function getDailyEmailLimit()
{
if($this->is_flagged)
return 0;
if(Carbon::createFromTimestamp($this->created_at)->diffInWeeks() == 0)
return 20;
if(Carbon::createFromTimestamp($this->created_at)->diffInWeeks() <= 2 && !$this->payment_id)
return 20;
if($this->isPaid()){
$limit = $this->paid_plan_email_quota;
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 100;

View File

@ -126,6 +126,26 @@ class CompanyPresenter extends EntityPresenter
}
}
public function address1()
{
return $this->entity->settings->address1;
}
public function address2()
{
return $this->entity->settings->address2;
}
public function qr_iban()
{
return $this->entity->getSetting('qr_iban');
}
public function besr_id()
{
return $this->entity->getSetting('besr_id');
}
public function getSpcQrCode($client_currency, $invoice_number, $balance_due_raw, $user_iban)
{
$settings = $this->entity->settings;

View File

@ -16,8 +16,12 @@ use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Models\Quote;
use App\Models\QuoteInvitation;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Services\PdfMaker\Designs\Utilities\DesignHelpers;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
@ -26,6 +30,7 @@ use App\Utils\Traits\MakesTemplateData;
use DB;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Str;
use League\CommonMark\CommonMarkConverter;
use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles;
@ -88,7 +93,7 @@ class TemplateEngine
private function setEntity()
{
if (strlen($this->entity) > 1 && strlen($this->entity_id) > 1) {
$class = 'App\Models\\'.ucfirst($this->entity);
$class = 'App\Models\\'.ucfirst(Str::camel($this->entity));
$this->entity_obj = $class::withTrashed()->where('id', $this->decodePrimaryKey($this->entity_id))->company()->first();
} else {
$this->mockEntity();
@ -99,7 +104,11 @@ class TemplateEngine
private function setSettingsObject()
{
if ($this->entity_obj) {
if($this->entity == 'purchase_order'){
$this->settings_entity = auth()->user()->company();
$this->settings = $this->settings_entity->settings;
}
elseif ($this->entity_obj) {
$this->settings_entity = $this->entity_obj->client;
$this->settings = $this->settings_entity->getMergedSettings();
} else {
@ -143,7 +152,10 @@ class TemplateEngine
$this->raw_body = $this->body;
$this->raw_subject = $this->subject;
if ($this->entity_obj) {
if($this->entity == 'purchase_order'){
$this->fakerValues();
}
elseif ($this->entity_obj) {
$this->entityValues($this->entity_obj->client->primary_contact()->first());
} else {
$this->fakerValues();
@ -198,7 +210,17 @@ class TemplateEngine
$data['footer'] = '';
$data['logo'] = auth()->user()->company()->present()->logo();
$data = array_merge($data, Helpers::sharedEmailVariables($this->entity_obj->client));
if($this->entity_obj->client)
$data = array_merge($data, Helpers::sharedEmailVariables($this->entity_obj->client));
else{
$data['signature'] = $this->settings->email_signature;
$data['settings'] = $this->settings;
$data['whitelabel'] = $this->entity_obj ? $this->entity_obj->company->account->isPaid() : true;
$data['company'] = $this->entity_obj ? $this->entity_obj->company : '';
$data['settings'] = $this->settings;
}
if ($email_style == 'custom') {
$wrapper = $this->settings_entity->getSetting('email_style_custom');
@ -243,6 +265,8 @@ class TemplateEngine
{
DB::connection(config('database.default'))->beginTransaction();
$vendor = false;
$client = Client::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
@ -289,12 +313,60 @@ class TemplateEngine
]);
}
$this->entity_obj->setRelation('invitations', $invitation);
$this->entity_obj->setRelation('client', $client);
$this->entity_obj->setRelation('company', auth()->user()->company());
$this->entity_obj->load('client');
$client->setRelation('company', auth()->user()->company());
$client->load('company');
if($this->entity == 'purchase_order')
{
$vendor = Vendor::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
]);
$contact = VendorContact::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
'vendor_id' => $vendor->id,
'is_primary' => 1,
'send_email' => true,
]);
$this->entity_obj = PurchaseOrder::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
'vendor_id' => $vendor->id,
]);
$invitation = PurchaseOrderInvitation::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
'purchase_order_id' => $this->entity_obj->id,
'vendor_contact_id' => $contact->id,
]);
}
if($vendor)
{
$this->entity_obj->setRelation('invitations', $invitation);
$this->entity_obj->setRelation('vendor', $vendor);
$this->entity_obj->setRelation('company', auth()->user()->company());
$this->entity_obj->load('vendor');
$vendor->setRelation('company', auth()->user()->company());
$vendor->load('company');
}
else
{
$this->entity_obj->setRelation('invitations', $invitation);
$this->entity_obj->setRelation('client', $client);
$this->entity_obj->setRelation('company', auth()->user()->company());
$this->entity_obj->load('client');
$client->setRelation('company', auth()->user()->company());
$client->load('company');
}
}
private function tearDown()

View File

@ -120,6 +120,9 @@ trait CompanySettingsSaver
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter') {
$value = 'integer';
if($key == 'besr_id')
$value = 'string';
if (! property_exists($settings, $key)) {
continue;
} elseif (! $this->checkAttribute($value, $settings->{$key})) {
@ -187,6 +190,9 @@ trait CompanySettingsSaver
if($key == 'gmail_sending_user_id')
$value = 'string';
if($key == 'besr_id')
$value = 'string';
if (! property_exists($settings, $key)) {
continue;
} elseif ($this->checkAttribute($value, $settings->{$key})) {

View File

@ -200,6 +200,36 @@ trait MakesTemplateData
$data['$task.tax_name3'] = ['value' => 'CA Sales Tax', 'label' => ctrans('texts.tax')];
$data['$task.line_total'] = ['value' => '$100.00', 'label' => ctrans('texts.line_total')];
$data['$vendor_name'] = &$data['$client_name'];
$data['$vendor.name'] = &$data['$client_name'];
$data['$vendor'] = &$data['$client_name'];
$data['$vendor.address1'] = &$data['$address1'];
$data['$vendor.address2'] = &$data['$address2'];
$data['$vendor_address'] = ['value' => '5 Kalamazoo Way\n Jimbuckeroo\n USA 90210', 'label' => ctrans('texts.address')];
$data['$vendor.address'] = &$data['$vendor_address'];
$data['$vendor.postal_code'] = ['value' => '90210', 'label' => ctrans('texts.postal_code')];
$data['$vendor.public_notes'] = $data['$invoice.public_notes'];
$data['$vendor.city'] = &$data['$company.city'];
$data['$vendor.state'] = &$data['$company.state'];
$data['$vendor.id_number'] = &$data['$id_number'];
$data['$vendor.vat_number'] = &$data['$vat_number'];
$data['$vendor.website'] = &$data['$website'];
$data['$vendor.phone'] = &$data['$phone'];
$data['$vendor.city_state_postal'] = &$data['$city_state_postal'];
$data['$vendor.postal_city_state'] = &$data['$postal_city_state'];
$data['$vendor.country'] = &$data['$country'];
$data['$vendor.email'] = &$data['$email'];
$data['$vendor.billing_address1'] = &$data['$vendor.address1'];
$data['$vendor.billing_address2'] = &$data['$vendor.address2'];
$data['$vendor.billing_city'] = &$data['$vendor.city'];
$data['$vendor.billing_state'] = &$data['$vendor.state'];
$data['$vendor.billing_postal_code'] = &$data['$vendor.postal_code'];
$data['$vendor.billing_country'] = &$data['$vendor.country'];
//$data['$paid_to_date'] = ;
// $data['$your_invoice'] = ;
// $data['$quote'] = ;

View File

@ -55,7 +55,7 @@ trait SettingsSaver
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter' || ($key == 'payment_terms' && strlen($settings->{$key}) >= 1) || ($key == 'valid_until' && strlen($settings->{$key}) >= 1)) {
$value = 'integer';
if($key == 'gmail_sending_user_id')
if($key == 'gmail_sending_user_id' || $key == 'besr_id')
$value = 'string';
if (! property_exists($settings, $key)) {

View File

@ -85,6 +85,7 @@
"setasign/fpdi": "^2.3",
"socialiteproviders/apple": "^5.2",
"socialiteproviders/microsoft": "^4.1",
"sprain/swiss-qr-bill": "^3.2",
"square/square": "13.0.0.20210721",
"stripe/stripe-php": "^7.50",
"symfony/http-client": "^5.2",

1042
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,51 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Database\Factories;
use App\Factory\InvoiceItemFactory;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use Illuminate\Database\Eloquent\Factories\Factory;
class PurchaseOrderFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = PurchaseOrder::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'status_id' => Invoice::STATUS_SENT,
'number' => $this->faker->ean13(),
'discount' => $this->faker->numberBetween(1, 10),
'is_amount_discount' => (bool) random_int(0, 1),
'tax_name1' => 'GST',
'tax_rate1' => 10,
'tax_name2' => 'VAT',
'tax_rate2' => 17.5,
'is_deleted' => false,
'po_number' => $this->faker->text(10),
'date' => $this->faker->date(),
'due_date' => $this->faker->date(),
'line_items' => InvoiceItemFactory::generate(5),
'terms' => $this->faker->text(500),
];
}
}

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddFlagToAccountsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('accounts', function (Blueprint $table) {
$table->boolean('is_flagged')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@ -129,6 +129,9 @@ Route::group(['middleware' => ['throttle:100,1', 'api_db', 'token_auth', 'locale
Route::post('preview', 'PreviewController@show')->name('preview.show');
Route::post('live_preview', 'PreviewController@live')->name('preview.live');
Route::post('preview/purchase_order', 'PreviewPurchaseOrderController@show')->name('preview_purchase_order.show');
Route::post('live_preview/purchase_order', 'PreviewPurchaseOrderController@live')->name('preview_purchase_order.live');
Route::resource('products', 'ProductController'); // name = (products. index / create / show / update / destroy / edit
Route::post('products/bulk', 'ProductController@bulk')->name('products.bulk');
Route::put('products/{product}/upload', 'ProductController@upload');

View File

@ -20,6 +20,8 @@ Route::get('vendors', [VendorContactLoginController::class, 'catch'])->name('ven
Route::group(['middleware' => ['invite_db'], 'prefix' => 'vendor', 'as' => 'vendor.'], function () {
/*Invitation catches*/
Route::get('purchase_order/{invitation_key}', [InvitationController::class, 'purchaseOrder']);
Route::get('purchase_order/{invitation_key}/download', [InvitationController::class, 'download']);
// Route::get('purchase_order/{invitation_key}/download_pdf', 'PurchaseOrderController@downloadPdf')->name('recurring_invoice.download_invitation_key');
// Route::get('purchase_order/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload');
@ -40,4 +42,7 @@ Route::group(['middleware' => ['auth:vendor', 'vendor_locale', 'domain_db'], 'pr
});
Route::fallback('BaseController@notFoundVendor');

View File

@ -56,7 +56,7 @@ class CompanySettingsTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-Token' => $this->token,
])->put('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $this->company->toArray());
])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $this->company->toArray());
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
}
@ -78,11 +78,13 @@ class CompanySettingsTest extends TestCase
$this->company->saveSettings($settings, $this->company);
$response = false;
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-Token' => $this->token,
])->put('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $this->company->toArray());
])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $this->company->toArray());
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
@ -109,7 +111,7 @@ class CompanySettingsTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-Token' => $this->token,
])->put('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $this->company->toArray());
])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $this->company->toArray());
$response->assertStatus(200);
@ -135,7 +137,7 @@ class CompanySettingsTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-Token' => $this->token,
])->put('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $this->company->toArray());
])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $this->company->toArray());
$response->assertStatus(200);
@ -162,7 +164,7 @@ class CompanySettingsTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-Token' => $this->token,
])->put('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $this->company->toArray());
])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $this->company->toArray());
$response->assertStatus(200);
@ -185,7 +187,7 @@ class CompanySettingsTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-Token' => $this->token,
])->post('/api/v1/companies?include=company', $this->company->toArray());
])->postJson('/api/v1/companies?include=company', $this->company->toArray());
$arr = $response->json();
$response->assertStatus(200);
@ -203,7 +205,7 @@ class CompanySettingsTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-Token' => $this->token,
])->post('/api/v1/companies?include=company', $this->company->toArray());
])->postJson('/api/v1/companies?include=company', $this->company->toArray());
$arr = $response->json();
$response->assertStatus(200);
@ -221,7 +223,7 @@ class CompanySettingsTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-Token' => $this->token,
])->post('/api/v1/companies?include=company', $this->company->toArray());
])->postJson('/api/v1/companies?include=company', $this->company->toArray());
$arr = $response->json();
$response->assertStatus(200);
@ -239,7 +241,7 @@ class CompanySettingsTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-Token' => $this->token,
])->post('/api/v1/companies?include=company', $this->company->toArray());
])->postJson('/api/v1/companies?include=company', $this->company->toArray());
$arr = $response->json();
$response->assertStatus(200);

View File

@ -47,6 +47,31 @@ class PreviewTest extends TestCase
$response->assertStatus(200);
}
public function testPurchaseOrderPreviewRoute()
{
$data = $this->getData();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/preview/purchase_order', $data);
$response->assertStatus(200);
}
public function testPurchaseOrderPreviewHtmlResponse()
{
$data = $this->getData();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/preview/purchase_order?html=true', $data);
$response->assertStatus(200);
}
public function testPreviewHtmlResponse()
{
$data = $this->getData();