Merge pull request #7523 from turbo124/v5-develop

Purchase Orders
This commit is contained in:
David Bomba 2022-06-08 08:28:31 +10:00 committed by GitHub
commit f4aeee59fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1904 additions and 170 deletions

View File

@ -116,6 +116,9 @@ class CompanySettings extends BaseSettings
public $project_number_pattern = ''; //@implemented
public $project_number_counter = 1; //@implemented
public $purchase_order_number_pattern = ''; //@implemented
public $purchase_order_number_counter = 1; //@implemented
public $shared_invoice_quote_counter = false; //@implemented
public $shared_invoice_credit_counter = false; //@implemented
public $recurring_number_prefix = ''; //@implemented
@ -133,6 +136,13 @@ class CompanySettings extends BaseSettings
public $invoice_design_id = 'Wpmbk5ezJn'; //@implemented
public $quote_design_id = 'Wpmbk5ezJn'; //@implemented
public $credit_design_id = 'Wpmbk5ezJn'; //@implemented
public $purchase_order_design_id = 'Wpmbk5ezJn';
public $purchase_order_footer = ''; //@implemented
public $purchase_order_terms = ''; //@implemented
public $purchase_order_public_notes = ''; //@implemented
public $require_purchase_order_signature = false; //@TODO ben to confirm
public $invoice_footer = ''; //@implemented
public $credit_footer = ''; //@implemented
public $credit_terms = ''; //@implemented
@ -170,6 +180,8 @@ class CompanySettings extends BaseSettings
public $email_subject_payment = ''; //@implemented
public $email_subject_payment_partial = ''; //@implemented
public $email_subject_statement = ''; //@implemented
public $email_subject_purchase_order = ''; //@implemented
public $email_template_purchase_order = ''; //@implemented
public $email_template_invoice = ''; //@implemented
public $email_template_credit = ''; //@implemented
public $email_template_quote = ''; //@implemented
@ -276,10 +288,16 @@ class CompanySettings extends BaseSettings
public $email_from_name = '';
public $auto_archive_invoice_cancelled = false;
public $purchase_order_number_counter = 1; //TODO
public static $casts = [
'email_subject_purchase_order' => 'string',
'email_template_purchase_order' => 'string',
'require_purchase_order_signature' => 'bool',
'purchase_order_public_notes' => 'string',
'purchase_order_terms' => 'string',
'purchase_order_design_id' => 'string',
'purchase_order_footer' => 'string',
'purchase_order_number_pattern' => 'string',
'purchase_order_number_counter' => 'int',
'page_numbering_alignment' => 'string',
'page_numbering' => 'bool',
'auto_archive_invoice_cancelled' => 'bool',
@ -531,6 +549,7 @@ class CompanySettings extends BaseSettings
'invoice_design_id',
'quote_design_id',
'credit_design_id',
'purchase_order_design_id',
];
/**
@ -629,6 +648,25 @@ class CompanySettings extends BaseSettings
'$client.phone',
'$contact.email',
],
'vendor_details' => [
'$vendor.name',
'$vendor.number',
'$vendor.vat_number',
'$vendor.address1',
'$vendor.address2',
'$vendor.city_state_postal',
'$vendor.country',
'$vendor.phone',
'$contact.email',
],
'purchase_order_details' => [
'$purchase_order.number',
'$purchase_order.po_number',
'$purchase_order.date',
'$purchase_order.due_date',
'$purchase_order.total',
'$purchase_order.balance_due',
],
'company_details' => [
'$company.name',
'$company.id_number',

View File

@ -12,7 +12,6 @@
namespace App\Events\Invoice;
use App\Models\Company;
use App\Models\InvoiceInvitation;
use Illuminate\Queue\SerializesModels;
/**
@ -35,12 +34,12 @@ class InvoiceWasEmailedAndFailed
/**
* Create a new event instance.
*
* @param InvoiceInvitation $invitation
* @param $invitation
* @param Company $company
* @param string $errors
* @param array $event_vars
*/
public function __construct(InvoiceInvitation $invitation, Company $company, string $message, string $template, array $event_vars)
public function __construct($invitation, Company $company, string $message, string $template, array $event_vars)
{
$this->invitation = $invitation;

View File

@ -52,7 +52,10 @@ class InvoiceItemSum
$this->invoice = $invoice;
$this->currency = $this->invoice->client->currency();
if($this->invoice->client)
$this->currency = $this->invoice->client->currency();
else
$this->currency = $this->invoice->vendor->currency();
$this->line_items = [];
}

View File

@ -46,7 +46,10 @@ class InvoiceItemSumInclusive
$this->invoice = $invoice;
$this->currency = $this->invoice->client->currency();
if($this->invoice->client)
$this->currency = $this->invoice->client->currency();
else
$this->currency = $this->invoice->vendor->currency();
$this->line_items = [];
}

View File

@ -44,6 +44,8 @@ class InvoiceSum
private $gross_sub_total;
private $precision;
/**
* Constructs the object with Invoice and Settings object.
*
@ -53,8 +55,10 @@ class InvoiceSum
{
$this->invoice = $invoice;
// if(!$this->invoice->relationLoaded('client'))
// $this->invoice->load('client');
if($this->invoice->client)
$this->precision = $this->invoice->client->currency()->precision;
else
$this->precision = $this->invoice->vendor->currency()->precision;
$this->tax_map = new Collection;
}
@ -224,11 +228,19 @@ class InvoiceSum
return $this->invoice;
}
public function getPurchaseOrder()
{
$this->setCalculatedAttributes();
$this->invoice->saveQuietly();
return $this->invoice;
}
public function getRecurringInvoice()
{
$this->invoice->amount = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision);
$this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision);
$this->invoice->total_taxes = $this->getTotalTaxes();
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision);
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision);
$this->invoice->saveQuietly();
@ -247,13 +259,13 @@ class InvoiceSum
if ($this->invoice->amount != $this->invoice->balance) {
$paid_to_date = $this->invoice->amount - $this->invoice->balance;
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision) - $paid_to_date;
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision) - $paid_to_date;
} else {
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision);
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision);
}
}
/* Set new calculated total */
$this->invoice->amount = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision);
$this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision);
$this->invoice->total_taxes = $this->getTotalTaxes();

View File

@ -41,6 +41,7 @@ class InvoiceSumInclusive
private $sub_total;
private $precision;
/**
* Constructs the object with Invoice and Settings object.
*
@ -50,6 +51,11 @@ class InvoiceSumInclusive
{
$this->invoice = $invoice;
if($this->invoice->client)
$this->precision = $this->invoice->client->currency()->precision;
else
$this->precision = $this->invoice->vendor->currency()->precision;
$this->tax_map = new Collection;
}
@ -164,32 +170,15 @@ class InvoiceSumInclusive
private function calculateTotals()
{
//$this->total += $this->total_taxes;
// if (is_numeric($this->invoice->custom_value1)) {
// $this->total += $this->invoice->custom_value1;
// }
// if (is_numeric($this->invoice->custom_value2)) {
// $this->total += $this->invoice->custom_value2;
// }
// if (is_numeric($this->invoice->custom_value3)) {
// $this->total += $this->invoice->custom_value3;
// }
// if (is_numeric($this->invoice->custom_value4)) {
// $this->total += $this->invoice->custom_value4;
// }
return $this;
}
public function getRecurringInvoice()
{
$this->invoice->amount = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision);
$this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision);
$this->invoice->total_taxes = $this->getTotalTaxes();
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision);
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision);
$this->invoice->saveQuietly();
@ -229,6 +218,15 @@ class InvoiceSumInclusive
return $this->invoice;
}
public function getPurchaseOrder()
{
//Build invoice values here and return Invoice
$this->setCalculatedAttributes();
$this->invoice->saveQuietly();
return $this->invoice;
}
/**
* Build $this->invoice variables after
* calculations have been performed.
@ -240,14 +238,14 @@ class InvoiceSumInclusive
if ($this->invoice->amount != $this->invoice->balance) {
$paid_to_date = $this->invoice->amount - $this->invoice->balance;
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision) - $paid_to_date;
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision) - $paid_to_date;
} else {
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision);
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision);
}
}
/* Set new calculated total */
$this->invoice->amount = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision);
$this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision);
$this->invoice->total_taxes = $this->getTotalTaxes();

View File

@ -709,7 +709,6 @@ class InvoiceController extends BaseController
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
break;
case 'restore':
$this->invoice_repo->restore($invoice);

View File

@ -14,12 +14,14 @@ namespace App\Http\Controllers;
use App\Factory\PurchaseOrderFactory;
use App\Filters\PurchaseOrderFilters;
use App\Http\Requests\PurchaseOrder\ActionPurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\CreatePurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\DestroyPurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\EditPurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\ShowPurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\StorePurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\UpdatePurchaseOrderRequest;
use App\Jobs\Invoice\ZipInvoices;
use App\Models\Client;
use App\Models\PurchaseOrder;
use App\Repositories\PurchaseOrderRepository;
@ -174,8 +176,6 @@ class PurchaseOrderController extends BaseController
public function store(StorePurchaseOrderRequest $request)
{
$client = Client::find($request->get('client_id'));
$purchase_order = $this->purchase_order_repository->save($request->all(), PurchaseOrderFactory::create(auth()->user()->company()->id, auth()->user()->id));
$purchase_order = $purchase_order->service()
@ -410,4 +410,221 @@ class PurchaseOrderController extends BaseController
return $this->itemResponse($purchase_order->fresh());
}
/**
* Perform bulk actions on the list view.
*
* @return Collection
*
* @OA\Post(
* path="/api/v1/purchase_orders/bulk",
* operationId="bulkPurchaseOrderss",
* tags={"purchase_orders"},
* summary="Performs bulk actions on an array of purchase_orders",
* description="",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @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="Purchase Order IDS",
* 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()
{
$action = request()->input('action');
$ids = request()->input('ids');
$purchase_orders = PurchaseOrder::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get();
if (! $invoices) {
return response()->json(['message' => 'No Purchase Orders Found']);
}
/*
* Download Purchase Order/s
*/
if ($action == 'bulk_download' && $purchase_orders->count() > 1) {
$purchase_orders->each(function ($purchase_order) {
if (auth()->user()->cannot('view', $purchase_order)) {
nlog("access denied");
return response()->json(['message' => ctrans('text.access_denied')]);
}
});
ZipInvoices::dispatch($purchase_orders, $purchase_orders->first()->company, auth()->user());
return response()->json(['message' => ctrans('texts.sent_message')], 200);
}
/*
* Send the other actions to the switch
*/
$purchase_orders->each(function ($purchase_order, $key) use ($action) {
if (auth()->user()->can('edit', $purchase_order)) {
$this->performAction($purchase_order, $action, true);
}
});
/* Need to understand which permission are required for the given bulk action ie. view / edit */
return $this->listResponse(PurchaseOrder::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
}
/**
* @OA\Get(
* path="/api/v1/purchase_orders/{id}/{action}",
* operationId="actionPurchaseOrder",
* tags={"purchase_orders"},
* summary="Performs a custom action on an purchase order",
* description="Performs a custom action on an purchase order.
*
* The current range of actions are as follows
* - mark_paid
* - download
* - archive
* - delete
* - email",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Purchase Order Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Parameter(
* name="action",
* in="path",
* description="The action string to be performed",
* example="clone_to_quote",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the invoice object",
* @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\JsonContent(ref="#/components/schemas/Invoice"),
* ),
* @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"),
* ),
* )
* @param ActionPurchaseOrderRequest $request
* @param PurchaseOrder $purchase_order
* @param $action
* @return \App\Http\Controllers\Response|\Illuminate\Http\JsonResponse|Response|mixed|\Symfony\Component\HttpFoundation\StreamedResponse
*/
public function action(ActionPurchaseOrderRequest $request, PurchaseOrder $purchase_order, $action)
{
return $this->performAction($invoice, $action);
}
private function performAction(PurchaseOrder $purchase_order, $action, $bulk = false)
{
/*If we are using bulk actions, we don't want to return anything */
switch ($action) {
case 'mark_sent':
$purchase_order->service()->markSent()->save();
if (! $bulk) {
return $this->itemResponse($purchase_order);
}
break;
case 'download':
$file = $purchase_order->service()->getPurchaseOrderPdf();
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
break;
case 'restore':
$this->purchase_order_repository->restore($purchase_order);
if (! $bulk) {
return $this->listResponse($purchase_order);
}
break;
case 'archive':
$this->purchase_order_repository->archive($purchase_order);
if (! $bulk) {
return $this->listResponse($purchase_order);
}
break;
case 'delete':
$this->purchase_order_repository->delete($purchase_order);
if (! $bulk) {
return $this->listResponse($purchase_order);
}
break;
case 'email':
//check query parameter for email_type and set the template else use calculateTemplate
default:
return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400);
break;
}
}
}

View File

@ -0,0 +1,61 @@
<?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\PurchaseOrder;
use App\Http\Requests\Request;
use App\Models\PurchaseOrder;
use App\Utils\Traits\MakesHash;
class ActionPurchaseOrderRequest extends Request
{
use MakesHash;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
private $error_msg;
// private $invoice;
public function authorize() : bool
{
return auth()->user()->can('edit', $this->purchase_order);
}
public function rules()
{
return [
'action' => 'required'
];
}
protected function prepareForValidation()
{
$input = $this->all();
if($this->action){
$input['action'] = $this->action;
} elseif (!array_key_exists('action', $input) ) {
$this->error_msg = 'Action is a required field';
}
$this->replace($input);
}
public function messages()
{
return [
'action' => $this->error_msg,
];
}
}

View File

@ -38,8 +38,7 @@ class StorePurchaseOrderRequest extends Request
{
$rules = [];
$rules['client_id'] = 'required';
$rules['vendor_id'] = 'required';
$rules['number'] = ['nullable', Rule::unique('purchase_orders')->where('company_id', auth()->user()->company()->id)];
$rules['discount'] = 'sometimes|numeric';

View File

@ -55,7 +55,6 @@ class UpdatePurchaseOrderRequest extends Request
$input = $this->decodePrimaryKeys($input);
$input['id'] = $this->purchase_order->id;
$this->replace($input);

View File

@ -1,11 +1,11 @@
<?php
/**
* Entity Ninja (https://entityninja.com).
* Invoice Ninja (https://entityninja.com).
*
* @link https://github.com/entityninja/entityninja source repository
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Entity Ninja LLC (https://entityninja.com)
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/

View File

@ -49,8 +49,8 @@ class SystemMaintenance implements ShouldQueue
nlog("Starting System Maintenance");
// if(Ninja::isHosted())
// return;
if(Ninja::isHosted())
return;
$delete_pdf_days = config('ninja.maintenance.delete_pdfs');

View File

@ -0,0 +1,221 @@
<?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\Jobs\Vendor;
use App\Exceptions\FilePermissionsFailure;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\Credit;
use App\Models\CreditInvitation;
use App\Models\Design;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Quote;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoice;
use App\Models\RecurringInvoiceInvitation;
use App\Services\PdfMaker\Design as PdfDesignModel;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\PdfMaker as PdfMakerService;
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\NumberFormatter;
use App\Utils\Traits\Pdf\PDF;
use App\Utils\Traits\Pdf\PageNumbering;
use App\Utils\Traits\Pdf\PdfMaker;
use App\Utils\VendorHtmlEngine;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Storage;
use setasign\Fpdi\PdfParser\StreamReader;
class CreatePurchaseOrderPdf implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, NumberFormatter, MakesInvoiceHtml, PdfMaker, MakesHash, PageNumbering;
public $entity;
public $company;
public $contact;
private $disk;
public $invitation;
public $entity_string = '';
public $vendor;
/**
* Create a new job instance.
*
* @param $invitation
*/
public function __construct($invitation, $disk = 'public')
{
$this->invitation = $invitation;
$this->company = $invitation->company;
$this->entity = $invitation->purchase_order;
$this->entity_string = 'purchase_order';
$this->contact = $invitation->contact;
$this->vendor = $invitation->contact->vendor;
$this->vendor->load('company');
$this->disk = Ninja::isHosted() ? config('filesystems.default') : $disk;
}
public function handle()
{
MultiDB::setDb($this->company->db);
/* Forget the singleton*/
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->locale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->generate($this->invitation);
}
$entity_design_id = '';
$path = $this->vendor->purchase_order_filepath($this->invitation);
$entity_design_id = 'purchase_order_design_id';
$file_path = $path.$this->entity->numberFormatter().'.pdf';
$entity_design_id = $this->entity->design_id ? $this->entity->design_id : $this->decodePrimaryKey('Wpmbk5ezJn');
$design = Design::find($entity_design_id);
/* Catch all in case migration doesn't pass back a valid design */
if(!$design)
$design = Design::find(2);
$html = new VendorHtmlEngine($this->invitation);
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' => $this->vendor,
'entity' => $this->entity,
'pdf_variables' => (array) $this->company->settings->pdf_variables,
'$product' => $design->design->product,
'variables' => $variables,
]),
'variables' => $variables,
'options' => [
'all_pages_header' => $this->entity->company->getSetting('all_pages_header'),
'all_pages_footer' => $this->entity->company->getSetting('all_pages_footer'),
],
'process_markdown' => $this->entity->company->markdown_enabled,
];
$maker = new PdfMakerService($state);
$maker
->design($template)
->build();
$pdf = null;
try {
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, $this->company);
if($numbered_pdf)
$pdf = $numbered_pdf;
}
else {
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, $this->company);
if($numbered_pdf)
$pdf = $numbered_pdf;
}
} catch (\Exception $e) {
nlog(print_r($e->getMessage(), 1));
}
if (config('ninja.log_pdf_html')) {
info($maker->getCompiledHTML());
}
if ($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

@ -0,0 +1,42 @@
<?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\Models\Presenters;
/**
* Class VendorContactPresenter.
*/
class VendorContactPresenter extends EntityPresenter
{
/**
* @return string
*/
public function name()
{
$contact_name = $this->entity->first_name.' '.$this->entity->last_name;
if (strlen($contact_name) > 1) {
return $contact_name;
}
return $this->entity->vendor->present()->name();
}
public function first_name()
{
return $this->entity->first_name ?: '';
}
public function last_name()
{
return $this->entity->last_name ?: '';
}
}

View File

@ -12,7 +12,10 @@
namespace App\Models;
use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Jobs\Entity\CreateEntityPdf;
use App\Jobs\Vendor\CreatePurchaseOrderPdf;
use App\Services\PurchaseOrder\PurchaseOrderService;
use App\Utils\Ninja;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -159,20 +162,20 @@ class PurchaseOrder extends BaseModel
if(!$invitation)
throw new \Exception('Hard fail, could not create an invitation - is there a valid contact?');
$file_path = $this->client->credit_filepath($invitation).$this->numberFormatter().'.pdf';
$file_path = $this->vendor->purchase_order_filepath($invitation).$this->numberFormatter().'.pdf';
if(Ninja::isHosted() && $portal && Storage::disk(config('filesystems.default'))->exists($file_path)){
return Storage::disk(config('filesystems.default'))->{$type}($file_path);
}
elseif(Ninja::isHosted() && $portal){
$file_path = CreateEntityPdf::dispatchNow($invitation,config('filesystems.default'));
$file_path = CreatePurchaseOrderPdf::dispatchNow($invitation,config('filesystems.default'));
return Storage::disk(config('filesystems.default'))->{$type}($file_path);
}
if(Storage::disk('public')->exists($file_path))
return Storage::disk('public')->{$type}($file_path);
$file_path = CreateEntityPdf::dispatchNow($invitation);
$file_path = CreatePurchaseOrderPdf::dispatchNow($invitation);
return Storage::disk('public')->{$type}($file_path);
}
@ -193,7 +196,7 @@ class PurchaseOrder extends BaseModel
public function service()
{
return new PurchaseOrderService($this);
return new PurchaseOrderService($this);
}
public function invoices()
@ -211,4 +214,17 @@ class PurchaseOrder extends BaseModel
return $this->morphMany(Document::class, 'documentable');
}
public function calc()
{
$purchase_order_calc = null;
if ($this->uses_inclusive_taxes) {
$purchase_order_calc = new InvoiceSumInclusive($this);
} else {
$purchase_order_calc = new InvoiceSum($this);
}
return $purchase_order_calc->build();
}
}

View File

@ -4,6 +4,7 @@
namespace App\Models;
use App\Utils\Ninja;
use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates;
use Carbon\Carbon;
@ -77,5 +78,30 @@ class PurchaseOrderInvitation extends BaseModel
$this->save();
}
public function getPortalLink() :string
{
if(Ninja::isHosted())
$domain = $this->company->domain();
else
$domain = config('ninja.app_url');
switch ($this->company->portal_mode) {
case 'subdomain':
return $domain.'/vendor/';
break;
case 'iframe':
return $domain.'/vendor/';
break;
case 'domain':
return $domain.'/vendor/';
break;
default:
return '';
break;
}
}
}

View File

@ -11,9 +11,13 @@
namespace App\Models;
use App\DataMapper\CompanySettings;
use App\Models\Presenters\VendorPresenter;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\NumberFormatter;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
use Laracasts\Presenter\PresentableTrait;
class Vendor extends BaseModel
@ -22,6 +26,7 @@ class Vendor extends BaseModel
use Filterable;
use GeneratesCounter;
use PresentableTrait;
use AppSetup;
protected $fillable = [
'name',
@ -95,6 +100,22 @@ class Vendor extends BaseModel
return $this->hasMany(Activity::class);
}
public function currency()
{
$currencies = Cache::get('currencies');
if(!$currencies)
$this->buildCache(true);
if(!$this->currency_id)
$this->currency_id = 1;
return $currencies->filter(function ($item) {
return $item->id == $this->currency_id;
})->first();
}
public function company()
{
return $this->belongsTo(Company::class);
@ -109,4 +130,54 @@ class Vendor extends BaseModel
{
return ctrans('texts.vendor');
}
public function setCompanyDefaults($data, $entity_name) :array
{
$defaults = [];
if (! (array_key_exists('terms', $data) && strlen($data['terms']) > 1)) {
$defaults['terms'] = $this->getSetting($entity_name.'_terms');
} elseif (array_key_exists('terms', $data)) {
$defaults['terms'] = $data['terms'];
}
if (! (array_key_exists('footer', $data) && strlen($data['footer']) > 1)) {
$defaults['footer'] = $this->getSetting($entity_name.'_footer');
} elseif (array_key_exists('footer', $data)) {
$defaults['footer'] = $data['footer'];
}
if (strlen($this->public_notes) >= 1) {
$defaults['public_notes'] = $this->public_notes;
}
return $defaults;
}
public function getSetting($setting)
{
if ((property_exists($this->company->settings, $setting) != false) && (isset($this->company->settings->{$setting}) !== false)) {
return $this->company->settings->{$setting};
}
elseif( property_exists(CompanySettings::defaults(), $setting) ) {
return CompanySettings::defaults()->{$setting};
}
return '';
}
public function purchase_order_filepath($invitation)
{
$contact_key = $invitation->contact->contact_key;
return $this->company->company_key.'/'.$this->vendor_hash.'/'.$contact_key.'/purchase_orders/';
}
public function country()
{
return $this->belongsTo(Country::class);
}
}

View File

@ -11,6 +11,7 @@
namespace App\Models;
use App\Models\Presenters\VendorContactPresenter;
use App\Notifications\ClientContactResetPassword;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Translation\HasLocalePreference;
@ -35,6 +36,8 @@ class VendorContact extends Authenticatable implements HasLocalePreference
protected $touches = ['vendor'];
protected $presenter = VendorContactPresenter::class;
/* Allow microtime timestamps */
protected $dateFormat = 'Y-m-d H:i:s.u';

View File

@ -11,9 +11,10 @@
namespace App\Repositories;
use App\Factory\PurchaseOrderInvitationFactory;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Models\VendorContact;
use App\Utils\Traits\MakesHash;
class PurchaseOrderRepository extends BaseRepository
@ -27,13 +28,81 @@ class PurchaseOrderRepository extends BaseRepository
public function save(array $data, PurchaseOrder $purchase_order) : ?PurchaseOrder
{
$purchase_order->fill($data);
$purchase_order->save();
if (isset($data['invitations'])) {
$invitations = collect($data['invitations']);
/* Get array of Keys which have been removed from the invitations array and soft delete each invitation */
$purchase_order->invitations->pluck('key')->diff($invitations->pluck('key'))->each(function ($invitation) {
$invitation = PurchaseOrderInvitation::where('key', $invitation)->first();
if ($invitation)
$invitation->delete();
});
foreach ($data['invitations'] as $invitation) {
//if no invitations are present - create one.
if (! $this->getInvitation($invitation)) {
if (isset($invitation['id']))
unset($invitation['id']);
//make sure we are creating an invite for a contact who belongs to the client only!
$contact = VendorContact::find($invitation['vendor_contact_id']);
if ($contact && $purchase_order->vendor_id == $contact->vendor_id) {
$new_invitation = PurchaseOrderInvitation::withTrashed()
->where('vendor_contact_id', $contact->id)
->where('purchase_order_id', $purchase_order->id)
->first();
if ($new_invitation && $new_invitation->trashed()) {
$new_invitation->restore();
} else {
$new_invitation = PurchaseOrderInvitationFactory::create($purchase_order->company_id, $purchase_order->user_id);
$new_invitation->purchase_order_id = $purchase_order->id;
$new_invitation->vendor_contact_id = $contact->id;
$new_invitation->key = $this->createDbHash($purchase_order->company->db);
$new_invitation->save();
}
}
}
}
}
/* If no invitations have been created, this is our fail safe to maintain state*/
if ($purchase_order->invitations()->count() == 0)
$purchase_order->service()->createInvitations();
/* Recalculate invoice amounts */
$purchase_order = $purchase_order->calc()->getPurchaseOrder();
return $purchase_order;
}
public function getInvitationByKey($key) :?PurchaseOrderInvitation
{
return PurchaseOrderInvitation::where('key', $key)->first();
}
public function getInvitation($invitation, $resource=null)
{
if (is_array($invitation) && ! array_key_exists('key', $invitation))
return false;
$invitation = PurchaseOrderInvitation::where('key', $invitation['key'])->first();
return $invitation;
}
}

View File

@ -35,6 +35,9 @@ class Design extends BaseDesign
/** @var App\Models\Client */
public $client;
/** @var App\Models\Vendor */
public $vendor;
/** Global state of the design, @var array */
public $context;
@ -69,6 +72,8 @@ class Design extends BaseDesign
const DELIVERY_NOTE = 'delivery_note';
const STATEMENT = 'statement';
const PURCHASE_ORDER = 'purchase_order';
public function __construct(string $design = null, array $options = [])
{
@ -113,6 +118,10 @@ class Design extends BaseDesign
'id' => 'client-details',
'elements' => $this->clientDetails(),
],
'vendor-details' => [
'id' => 'vendor-details',
'elements' => $this->vendorDetails(),
],
'entity-details' => [
'id' => 'entity-details',
'elements' => $this->entityDetails(),
@ -188,10 +197,29 @@ class Design extends BaseDesign
return $elements;
}
public function vendorDetails(): array
{
$elements = [];
if(!$this->vendor)
return $elements;
$variables = $this->context['pdf_variables']['vendor_details'];
foreach ($variables as $variable) {
$elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-' . substr($variable, 1)]];
}
return $elements;
}
public function clientDetails(): array
{
$elements = [];
if(!$this->client)
return $elements;
if ($this->type == self::DELIVERY_NOTE) {
$elements = [
['element' => 'p', 'content' => ctrans('texts.delivery_note'), 'properties' => ['data-ref' => 'delivery_note-label', 'style' => 'font-weight: bold; text-transform: uppercase']],
@ -224,6 +252,8 @@ class Design extends BaseDesign
public function entityDetails(): array
{
if ($this->type === 'statement') {
$s_date = $this->translateDate(now(), $this->client->date_format(), $this->client->locale());
@ -258,6 +288,12 @@ class Design extends BaseDesign
$variables = $this->context['pdf_variables']['credit_details'];
}
if($this->vendor){
$variables = $this->context['pdf_variables']['purchase_order_details'];
}
$elements = [];
// We don't want to show account balance or invoice total on PDF.. or any amount with currency.

View File

@ -30,6 +30,10 @@ trait DesignHelpers
{
$this->syncPdfVariables();
if (isset($this->context['vendor'])) {
$this->vendor = $this->context['vendor'];
}
if (isset($this->context['client'])) {
$this->client = $this->context['client'];
}

View File

@ -1,11 +1,18 @@
<?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\Services\PurchaseOrder;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Vendor;
use App\Models\PurchaseOrder;
use App\Services\AbstractService;
use App\Utils\Traits\GeneratesCounter;
@ -15,15 +22,15 @@ class ApplyNumber extends AbstractService
{
use GeneratesCounter;
private Client $client;
public Vendor $vendor;
private PurchaseOrder $purchase_order;
private bool $completed = true;
public function __construct(Client $client, PurchaseOrder $purchase_order)
public function __construct(Vendor $vendor, PurchaseOrder $purchase_order)
{
$this->client = $client;
$this->vendor = $vendor;
$this->purchase_order = $purchase_order;
}
@ -43,7 +50,7 @@ class ApplyNumber extends AbstractService
$x=1;
do{
try{
$this->purchase_order->number = $this->getNextPurchaseOrderNumber($this->client, $this->purchase_order);
$this->purchase_order->number = $this->getNextPurchaseOrderNumber($this->purchase_order);
$this->purchase_order->saveQuietly();
$this->completed = false;
}

View File

@ -1,10 +1,19 @@
<?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\Services\PurchaseOrder;
use App\Factory\PurchaseOrderInvitationFactory;
use App\Factory\VendorContactFactory;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Services\AbstractService;
@ -21,17 +30,19 @@ class CreateInvitations extends AbstractService
{
$this->purchase_order = $purchase_order;
}
private function createBlankContact()
{
$new_contact = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id);
$new_contact->client_id = $this->purchase_order->client_id;
$new_contact = VendorContactFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id);
$new_contact->vendor_id = $this->purchase_order->vendor_id;
$new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true;
$new_contact->save();
}
public function run()
{
$contacts = $this->purchase_order->vendor->contacts;
$contacts = $this->purchase_order->vendor->contacts()->where('send_email', true)->get();
if($contacts->count() == 0){
$this->createBlankContact();
@ -40,19 +51,25 @@ class CreateInvitations extends AbstractService
$contacts = $this->purchase_order->vendor->contacts;
}
$contacts->each(function ($contact) {
$invitation = PurchaseOrderInvitation::whereCompanyId($this->purchase_order->company_id)
->whereClientContactId($contact->id)
->whereCreditId($this->purchase_order->id)
$invitation = PurchaseOrderInvitation::where('company_id', $this->purchase_order->company_id)
->where('vendor_contact_id', $contact->id)
->where('purchase_order_id', $this->purchase_order->id)
->withTrashed()
->first();
if (! $invitation) {
$ii = PurchaseOrderInvitation::create($this->purchase_order->company_id, $this->purchase_order->user_id);
try{
$ii = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id);
$ii->key = $this->createDbHash($this->purchase_order->company->db);
$ii->purchase_order_id = $this->purchase_order->id;
$ii->vendor_contact_id = $contact->id;
$ii->save();
}
catch(\Exception $e){
nlog($e->getMessage());
}
} elseif (! $contact->send_email) {
$invitation->delete();
}
@ -66,7 +83,7 @@ class CreateInvitations extends AbstractService
else{
$contact = $contacts->first();
$invitation = PurchaseOrder::where('company_id', $this->purchase_order->company_id)
$invitation = PurchaseOrderInvitation::where('company_id', $this->purchase_order->company_id)
->where('vendor_contact_id', $contact->id)
->where('purchase_order_id', $this->purchase_order->id)
->withTrashed()
@ -78,14 +95,13 @@ class CreateInvitations extends AbstractService
}
}
$ii = PurchaseOrderInvitation::create($this->purchase_order->company_id, $this->purchase_order->user_id);
$ii = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id);
$ii->key = $this->createDbHash($this->purchase_order->company->db);
$ii->purchase_order_id = $this->purchase_order->id;
$ii->vendor_contact_id = $contact->id;
$ii->save();
}
return $this->purchase_order;
}
}

View File

@ -0,0 +1,58 @@
<?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\Services\PurchaseOrder;
use App\Jobs\Vendor\CreatePurchaseOrderPdf;
use App\Models\PurchaseOrder;
use App\Models\VendorContact;
use App\Services\AbstractService;
use App\Utils\TempFile;
use Illuminate\Support\Facades\Storage;
class GetPurchaseOrderPdf extends AbstractService
{
public function __construct(PurchaseOrder $purchase_order, VendorContact $contact = null)
{
$this->purchase_order = $purchase_order;
$this->contact = $contact;
}
public function run()
{
if (! $this->contact) {
$this->contact = $this->purchase_order->vendor->contacts()->where('send_email', true)->first();
}
$invitation = $this->purchase_order->invitations()->where('vendor_contact_id', $this->contact->id)->first();
if(!$invitation)
$invitation = $this->purchase_order->invitations()->first();
$path = $this->purchase_order->vendor->purchase_order_filepath($invitation);
$file_path = $path.$this->purchase_order->numberFormatter().'.pdf';
// $disk = 'public';
$disk = config('filesystems.default');
$file = Storage::disk($disk)->exists($file_path);
if (! $file) {
$file_path = CreatePurchaseOrderPdf::dispatchNow($invitation);
}
return $file_path;
}
}

View File

@ -1,5 +1,13 @@
<?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\Services\PurchaseOrder;
@ -10,13 +18,13 @@ use App\Utils\Ninja;
class MarkSent
{
private $client;
private $vendor;
private $purchase_order;
public function __construct($client, $purchase_order)
public function __construct($vendor, $purchase_order)
{
$this->client = $client;
$this->vendor = $vendor;
$this->purchase_order = $purchase_order;
}

View File

@ -13,6 +13,9 @@ namespace App\Services\PurchaseOrder;
use App\Models\PurchaseOrder;
use App\Services\PurchaseOrder\ApplyNumber;
use App\Services\PurchaseOrder\CreateInvitations;
use App\Services\PurchaseOrder\GetPurchaseOrderPdf;
use App\Utils\Traits\MakesHash;
class PurchaseOrderService
@ -21,11 +24,63 @@ class PurchaseOrderService
public PurchaseOrder $purchase_order;
public function __construct($purchase_order)
public function __construct(PurchaseOrder $purchase_order)
{
$this->purchase_order = $purchase_order;
}
public function createInvitations()
{
$this->purchase_order = (new CreateInvitations($this->purchase_order))->run();
return $this;
}
public function applyNumber()
{
$this->invoice = (new ApplyNumber($this->purchase_order->vendor, $this->purchase_order))->run();
return $this;
}
public function fillDefaults()
{
// $settings = $this->purchase_order->client->getMergedSettings();
// //TODO implement design, footer, terms
// /* If client currency differs from the company default currency, then insert the client exchange rate on the model.*/
// if (!isset($this->purchase_order->exchange_rate) && $this->purchase_order->client->currency()->id != (int)$this->purchase_order->company->settings->currency_id)
// $this->purchase_order->exchange_rate = $this->purchase_order->client->currency()->exchange_rate;
// if (!isset($this->purchase_order->public_notes))
// $this->purchase_order->public_notes = $this->purchase_order->client->public_notes;
return $this;
}
public function getPurchaseOrderPdf($contact = null)
{
return (new GetPurchaseOrderPdf($this->purchase_order, $contact))->run();
}
public function setStatus($status)
{
$this->purchase_order->status_id = $status;
return $this;
}
public function markSent()
{
$this->purchase_order = (new MarkSent($this->purchase_order->vendor, $this->purchase_order))->run();
return $this;
}
/**
* Saves the purchase order.
* @return \App\Models\PurchaseOrder object
@ -37,45 +92,4 @@ class PurchaseOrderService
return $this->purchase_order;
}
public function fillDefaults()
{
$settings = $this->purchase_order->client->getMergedSettings();
//TODO implement design, footer, terms
/* If client currency differs from the company default currency, then insert the client exchange rate on the model.*/
if (!isset($this->purchase_order->exchange_rate) && $this->purchase_order->client->currency()->id != (int)$this->purchase_order->company->settings->currency_id)
$this->purchase_order->exchange_rate = $this->purchase_order->client->currency()->exchange_rate;
if (!isset($this->purchase_order->public_notes))
$this->purchase_order->public_notes = $this->purchase_order->client->public_notes;
return $this;
}
public function setStatus($status)
{
$this->purchase_order->status_id = $status;
return $this;
}
public function markSent()
{
$this->purchase_order = (new MarkSent($this->purchase_order->client, $this->purchase_order))->run();
return $this;
}
/**
* Applies the purchase order number.
* @return $this PurchaseOrderService object
*/
public function applyNumber()
{
$this->purchase_order = (new ApplyNumber($this->purchase_order->client, $this->purchase_order))->run();
return $this;
}
}

View File

@ -52,7 +52,7 @@ class Helpers
*
* @return null|string
*/
public function formatCustomFieldValue($custom_fields, $field, $value, Client $client = null): ?string
public function formatCustomFieldValue($custom_fields, $field, $value, $entity = null): ?string
{
$custom_field = '';
@ -67,7 +67,7 @@ class Helpers
switch ($custom_field) {
case 'date':
return is_null($client) ? $value : $this->translateDate($value, $client->date_format(), $client->locale());
return is_null($entity) ? $value : $this->translateDate($value, $entity->date_format(), $entity->locale());
break;
case 'switch':
@ -104,15 +104,15 @@ class Helpers
* Process reserved keywords on PDF.
*
* @param string $value
* @param Client $client
* @param Client|Company $entity
* @return null|string
*/
public static function processReservedKeywords(?string $value, Client $client): ?string
public static function processReservedKeywords(?string $value, $entity): ?string
{
if(!$value)
return '';
Carbon::setLocale($client->locale());
Carbon::setLocale($entity->locale());
$replacements = [
'literal' => [
@ -121,21 +121,21 @@ class Helpers
':QUARTER' => 'Q' . now()->quarter,
':WEEK_BEFORE' => \sprintf(
'%s %s %s',
Carbon::now()->subDays(7)->translatedFormat($client->date_format()),
Carbon::now()->subDays(7)->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->translatedFormat($client->date_format())
Carbon::now()->translatedFormat($entity->date_format())
),
':WEEK_AHEAD' => \sprintf(
'%s %s %s',
Carbon::now()->addDays(7)->translatedFormat($client->date_format()),
Carbon::now()->addDays(7)->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(14)->translatedFormat($client->date_format())
Carbon::now()->addDays(14)->translatedFormat($entity->date_format())
),
':WEEK' => \sprintf(
'%s %s %s',
Carbon::now()->translatedFormat($client->date_format()),
Carbon::now()->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(7)->translatedFormat($client->date_format())
Carbon::now()->addDays(7)->translatedFormat($entity->date_format())
),
],
'raw' => [

View File

@ -158,6 +158,10 @@ trait GeneratesCounter
return 'purchase_order_number_counter';
break;
case PurchaseOrder::class:
return 'purchase_order_number_counter';
break;
default:
return 'default_number_counter';
break;
@ -192,20 +196,6 @@ trait GeneratesCounter
return $this->replaceUserVars($credit, $entity_number);
}
/**
* Gets the next purchase order number.
*
* @param PurchaseOrder $purchase_order The purchase order
*
* @return string The next purchase order number.
*/
public function getNextPurchaseOrderNumber(Client $client, ?PurchaseOrder $purchase_order) :string
{
$entity_number = $this->getNextEntityNumber(PurchaseOrder::class, $client);
return $this->replaceUserVars($purchase_order, $entity_number);
}
/**
@ -363,6 +353,23 @@ trait GeneratesCounter
}
public function getNextPurchaseOrderNumber(PurchaseOrder $purchase_order) :string
{
$this->resetCompanyCounters($purchase_order->company);
$counter = $purchase_order->company->settings->purchase_order_number_counter;
$setting_entity = $purchase_order->company->settings->purchase_order_number_counter;
$purchase_order_number = $this->checkEntityNumber(PurchaseOrder::class, $purchase_order, $counter, $purchase_order->company->settings->counter_padding, $purchase_order->company->settings->purchase_order_number_pattern);
$this->incrementCounter($purchase_order->company, 'purchase_order_number_counter');
$entity_number = $purchase_order_number;
return $this->replaceUserVars($purchase_order, $entity_number);
}
/**
* Gets the next expense number.
*

View File

@ -0,0 +1,714 @@
<?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\Utils;
use App\Models\Country;
use App\Models\CreditInvitation;
use App\Models\GatewayType;
use App\Models\InvoiceInvitation;
use App\Models\PurchaseOrderInvitation;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoiceInvitation;
use App\Services\PdfMaker\Designs\Utilities\DesignHelpers;
use App\Utils\Ninja;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
use App\Utils\transformTranslations;
use Exception;
use Illuminate\Support\Facades\App;
class VendorHtmlEngine
{
use MakesDates;
public $entity;
public $invitation;
public $vendor;
public $contact;
public $company;
public $settings;
public $entity_calc;
public $entity_string;
private $helpers;
public function __construct($invitation)
{
$this->invitation = $invitation;
$this->entity_string = $this->resolveEntityString();
$this->entity = $invitation->purchase_order;
$this->company = $invitation->company;
$this->contact = $invitation->contact->load('vendor');
$this->vendor = $this->contact->vendor->load('company','country');
$this->entity->load('vendor');
$this->settings = $this->company->settings;
$this->entity_calc = $this->entity->calc();
$this->helpers = new Helpers();
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private function resolveEntityString()
{
switch ($this->invitation) {
case ($this->invitation instanceof InvoiceInvitation):
return 'invoice';
break;
case ($this->invitation instanceof CreditInvitation):
return 'credit';
break;
case ($this->invitation instanceof QuoteInvitation):
return 'quote';
break;
case ($this->invitation instanceof RecurringInvoiceInvitation):
return 'recurring_invoice';
break;
case ($this->invitation instanceof PurchaseOrderInvitation):
return 'purchase_order';
break;
default:
# code...
break;
}
}
public function buildEntityDataArray() :array
{
if (! $this->vendor->currency()) {
throw new Exception(debug_backtrace()[1]['function'], 1);
exit;
}
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($this->company->locale());
$t->replace(Ninja::transformTranslations($this->settings));
$data = [];
$data['$global_margin'] = ['value' => '6.35mm', 'label' => ''];
$data['$tax'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$app_url'] = ['value' => $this->generateAppUrl(), 'label' => ''];
$data['$from'] = ['value' => '', 'label' => ctrans('texts.from')];
$data['$to'] = ['value' => '', 'label' => ctrans('texts.to')];
$data['$total_tax_labels'] = ['value' => $this->totalTaxLabels(), 'label' => ctrans('texts.taxes')];
$data['$total_tax_values'] = ['value' => $this->totalTaxValues(), 'label' => ctrans('texts.taxes')];
$data['$line_tax_labels'] = ['value' => $this->lineTaxLabels(), 'label' => ctrans('texts.taxes')];
$data['$line_tax_values'] = ['value' => $this->lineTaxValues(), 'label' => ctrans('texts.taxes')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.date')];
$data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.due_date')];
$data['$partial_due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
$data['$dueDate'] = &$data['$due_date'];
$data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.payment_due')];
$data['$poNumber'] = ['value' => $this->entity->po_number, 'label' => ctrans('texts.po_number')];
$data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->company->date_format(), $this->company->locale()), 'label' => ctrans('texts.date')];
$data['$payment_button'] = ['value' => '<a class="button" href="'.$this->invitation->getPaymentLink().'">'.ctrans('texts.pay_now').'</a>', 'label' => ctrans('texts.pay_now')];
$data['$payment_link'] = ['value' => $this->invitation->getPaymentLink(), 'label' => ctrans('texts.pay_now')];
$data['$entity'] = ['value' => '', 'label' => ctrans('texts.purchase_order')];
$data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.purchase_order_number')];
$data['$number_short'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.purchase_order_number_short')];
$data['$entity.terms'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->terms), $this->company) ?: '', 'label' => ctrans('texts.invoice_terms')];
$data['$terms'] = &$data['$entity.terms'];
$data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_invoice').'</a>', 'label' => ctrans('texts.view_invoice')];
$data['$viewLink'] = &$data['$view_link'];
$data['$viewButton'] = &$data['$view_link'];
$data['$view_button'] = &$data['$view_link'];
$data['$paymentButton'] = &$data['$payment_button'];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.date')];
$data['$purchase_order.number'] = &$data['$number'];
$data['$purchase_order.date'] = &$data['$date'];
$data['$purchase_order.po_number'] = &$data['$poNumber'];
$data['$purchase_order.due_date'] = &$data['$due_date'];
$data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' =>''];
$data['$entity_number'] = &$data['$number'];
$data['$discount'] = ['value' => $this->entity->discount, 'label' => ctrans('texts.discount')];
$data['$subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.subtotal')];
$data['$gross_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getGrossSubTotal(), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.subtotal')];
if($this->entity->uses_inclusive_taxes)
$data['$net_subtotal'] = ['value' => Number::formatMoney(($this->entity_calc->getSubTotal() - $this->entity->total_taxes - $this->entity_calc->getTotalDiscount()), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.net_subtotal')];
else
$data['$net_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal() - $this->entity_calc->getTotalDiscount(), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.net_subtotal')];
if ($this->entity->partial > 0) {
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')];
$data['$amount_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')];
$data['$due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
} else {
if($this->entity->status_id == 1){
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->amount, $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.balance_due')];
$data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')];
}
else{
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.balance_due')];
$data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')];
}
}
// $data['$balance_due'] = $data['$balance_due'];
$data['$outstanding'] = &$data['$balance_due'];
$data['$partial_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')];
$data['$partial'] = &$data['$partial_due'];
$data['$total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.total')];
$data['$purchase_order.total'] = &$data['$total'];
$data['$amount'] = &$data['$total'];
$data['$amount_due'] = ['value' => &$data['$total']['value'], 'label' => ctrans('texts.amount_due')];
$data['$balance'] = ['value' => Number::formatMoney($this->entity_calc->getBalance(), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.balance')];
$data['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.taxes')];
$data['$user.name'] = ['value' => $this->entity->user->present()->name(), 'label' => ctrans('texts.name')];
$data['$user.first_name'] = ['value' => $this->entity->user->first_name, 'label' => ctrans('texts.first_name')];
$data['$user.last_name'] = ['value' => $this->entity->user->last_name, 'label' => ctrans('texts.last_name')];
$data['$created_by_user'] = &$data['$user.name'];
$data['$assigned_to_user'] = ['value' => $this->entity->assigned_user ? $this->entity->assigned_user->present()->name() : '', 'label' => ctrans('texts.name')];
$data['$public_notes'] = ['value' => $this->entity->public_notes, 'label' => ctrans("texts.public_notes")];
$data['$entity.public_notes'] = &$data['$public_notes'];
$data['$notes'] = &$data['$public_notes'];
$data['$purchase_order.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order1', $this->entity->custom_value1, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order1')];
$data['$purchase_order.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order2', $this->entity->custom_value2, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order2')];
$data['$purchase_order.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order3', $this->entity->custom_value3, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order3')];
$data['$purchase_order.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'purchase_order4', $this->entity->custom_value4, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'purchase_order4')];
$data['$vendor1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor1', $this->vendor->custom_value1, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor1')];
$data['$vendor2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor2', $this->vendor->custom_value2, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor2')];
$data['$vendor3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor3', $this->vendor->custom_value3, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor3')];
$data['$vendor4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor4', $this->vendor->custom_value4, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor4')];
$data['$vendor.custom1'] = &$data['$vendor1'];
$data['$vendor.custom2'] = &$data['$vendor2'];
$data['$vendor.custom3'] = &$data['$vendor3'];
$data['$vendor.custom4'] = &$data['$vendor4'];
$data['$address1'] = ['value' => $this->vendor->address1 ?: '&nbsp;', 'label' => ctrans('texts.address1')];
$data['$address2'] = ['value' => $this->vendor->address2 ?: '&nbsp;', 'label' => ctrans('texts.address2')];
$data['$id_number'] = ['value' => $this->vendor->id_number ?: '&nbsp;', 'label' => ctrans('texts.id_number')];
$data['$vendor.number'] = ['value' => $this->vendor->number ?: '&nbsp;', 'label' => ctrans('texts.number')];
$data['$vat_number'] = ['value' => $this->vendor->vat_number ?: '&nbsp;', 'label' => ctrans('texts.vat_number')];
$data['$website'] = ['value' => $this->vendor->present()->website() ?: '&nbsp;', 'label' => ctrans('texts.website')];
$data['$phone'] = ['value' => $this->vendor->present()->phone() ?: '&nbsp;', 'label' => ctrans('texts.phone')];
$data['$country'] = ['value' => isset($this->vendor->country->name) ? ctrans('texts.country_' . $this->vendor->country->name) : '', 'label' => ctrans('texts.country')];
$data['$country_2'] = ['value' => isset($this->vendor->country) ? $this->vendor->country->iso_3166_2 : '', 'label' => ctrans('texts.country')];
$data['$email'] = ['value' => isset($this->contact) ? $this->contact->email : 'no contact email on record', 'label' => ctrans('texts.email')];
if(str_contains($data['$email']['value'], 'example.com'))
$data['$email'] = ['value' => '', 'label' => ctrans('texts.email')];
$data['$vendor_name'] = ['value' => $this->vendor->present()->name() ?: '&nbsp;', 'label' => ctrans('texts.vendor_name')];
$data['$vendor.name'] = &$data['$vendor_name'];
$data['$vendor'] = &$data['$vendor_name'];
$data['$vendor.address1'] = &$data['$address1'];
$data['$vendor.address2'] = &$data['$address2'];
$data['$vendor_address'] = ['value' => $this->vendor->present()->address() ?: '&nbsp;', 'label' => ctrans('texts.address')];
$data['$vendor.address'] = &$data['$vendor_address'];
$data['$vendor.postal_code'] = ['value' => $this->vendor->postal_code ?: '&nbsp;', 'label' => ctrans('texts.postal_code')];
$data['$vendor.public_notes'] = ['value' => $this->vendor->public_notes ?: '&nbsp;', 'label' => ctrans('texts.notes')];
$data['$vendor.city'] = ['value' => $this->vendor->city ?: '&nbsp;', 'label' => ctrans('texts.city')];
$data['$vendor.state'] = ['value' => $this->vendor->state ?: '&nbsp;', 'label' => ctrans('texts.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['$city_state_postal'] = ['value' => $this->vendor->present()->cityStateZip($this->vendor->city, $this->vendor->state, $this->vendor->postal_code, false) ?: '&nbsp;', 'label' => ctrans('texts.city_state_postal')];
$data['$vendor.city_state_postal'] = &$data['$city_state_postal'];
$data['$postal_city_state'] = ['value' => $this->vendor->present()->cityStateZip($this->vendor->city, $this->vendor->state, $this->vendor->postal_code, true) ?: '&nbsp;', 'label' => ctrans('texts.postal_city_state')];
$data['$vendor.postal_city_state'] = &$data['$postal_city_state'];
$data['$vendor.country'] = &$data['$country'];
$data['$vendor.email'] = &$data['$email'];
$data['$vendor.billing_address'] = &$data['$vendor_address'];
$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['$vendor.currency'] = ['value' => $this->vendor->currency()->code, 'label' => ''];
$data['$paid_to_date'] = ['value' => Number::formatMoney($this->entity->paid_to_date, $this->vendor), 'label' => ctrans('texts.paid_to_date')];
$data['$contact.full_name'] = ['value' => $this->contact->present()->name(), 'label' => ctrans('texts.name')];
$data['$contact'] = &$data['$contact.full_name'];
$data['$contact.email'] = &$data['$email'];
$data['$contact.phone'] = ['value' => $this->contact->phone, 'label' => ctrans('texts.phone')];
$data['$contact.name'] = ['value' => isset($this->contact) ? $this->contact->present()->name() : $this->vendor->present()->name(), 'label' => ctrans('texts.contact_name')];
$data['$contact.first_name'] = ['value' => isset($this->contact) ? $this->contact->first_name : '', 'label' => ctrans('texts.first_name')];
$data['$firstName'] = &$data['$contact.first_name'];
$data['$contact.last_name'] = ['value' => isset($this->contact) ? $this->contact->last_name : '', 'label' => ctrans('texts.last_name')];
$data['$contact.custom1'] = ['value' => isset($this->contact) ? $this->contact->custom_value1 : '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'contact1')];
$data['$contact.custom2'] = ['value' => isset($this->contact) ? $this->contact->custom_value2 : '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'contact2')];
$data['$contact.custom3'] = ['value' => isset($this->contact) ? $this->contact->custom_value3 : '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'contact3')];
$data['$contact.custom4'] = ['value' => isset($this->contact) ? $this->contact->custom_value4 : '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'contact4')];
$data['$company.city_state_postal'] = ['value' => $this->company->present()->cityStateZip($this->settings->city, $this->settings->state, $this->settings->postal_code, false) ?: '&nbsp;', 'label' => ctrans('texts.city_state_postal')];
$data['$company.postal_city_state'] = ['value' => $this->company->present()->cityStateZip($this->settings->city, $this->settings->state, $this->settings->postal_code, true) ?: '&nbsp;', 'label' => ctrans('texts.postal_city_state')];
$data['$company.name'] = ['value' => $this->settings->name ?: ctrans('texts.untitled_account'), 'label' => ctrans('texts.company_name')];
$data['$account'] = &$data['$company.name'];
$data['$company.address1'] = ['value' => $this->settings->address1 ?: '&nbsp;', 'label' => ctrans('texts.address1')];
$data['$company.address2'] = ['value' => $this->settings->address2 ?: '&nbsp;', 'label' => ctrans('texts.address2')];
$data['$company.city'] = ['value' => $this->settings->city ?: '&nbsp;', 'label' => ctrans('texts.city')];
$data['$company.state'] = ['value' => $this->settings->state ?: '&nbsp;', 'label' => ctrans('texts.state')];
$data['$company.postal_code'] = ['value' => $this->settings->postal_code ?: '&nbsp;', 'label' => ctrans('texts.postal_code')];
$data['$company.country'] = ['value' => $this->getCountryName(), 'label' => ctrans('texts.country')];
$data['$company.country_2'] = ['value' => $this->getCountryCode(), 'label' => ctrans('texts.country')];
$data['$company.phone'] = ['value' => $this->settings->phone ?: '&nbsp;', 'label' => ctrans('texts.phone')];
$data['$company.email'] = ['value' => $this->settings->email ?: '&nbsp;', 'label' => ctrans('texts.email')];
$data['$company.vat_number'] = ['value' => $this->settings->vat_number ?: '&nbsp;', 'label' => ctrans('texts.vat_number')];
$data['$company.id_number'] = ['value' => $this->settings->id_number ?: '&nbsp;', 'label' => ctrans('texts.id_number')];
$data['$company.website'] = ['value' => $this->settings->website ?: '&nbsp;', 'label' => ctrans('texts.website')];
$data['$company.address'] = ['value' => $this->company->present()->address($this->settings) ?: '&nbsp;', 'label' => ctrans('texts.address')];
$data['$signature'] = ['value' => $this->settings->email_signature ?: '&nbsp;', 'label' => ''];
$data['$emailSignature'] = &$data['$signature'];
$logo = $this->company->present()->logo_base64($this->settings);
$data['$company.logo'] = ['value' => $logo ?: '&nbsp;', 'label' => ctrans('texts.logo')];
$data['$company_logo'] = &$data['$company.logo'];
$data['$company1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->vendor) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')];
$data['$company2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company2', $this->settings->custom_value2, $this->vendor) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company2')];
$data['$company3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company3', $this->settings->custom_value3, $this->vendor) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company3')];
$data['$company4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company4', $this->settings->custom_value4, $this->vendor) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company4')];
$data['$company.custom1'] = &$data['$company1'];
$data['$company.custom2'] = &$data['$company2'];
$data['$company.custom3'] = &$data['$company3'];
$data['$company.custom4'] = &$data['$company4'];
$data['$custom_surcharge1'] = ['value' => Number::formatMoney($this->entity->custom_surcharge1, $this->vendor) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'surcharge1')];
$data['$custom_surcharge2'] = ['value' => Number::formatMoney($this->entity->custom_surcharge2, $this->vendor) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'surcharge2')];
$data['$custom_surcharge3'] = ['value' => Number::formatMoney($this->entity->custom_surcharge3, $this->vendor) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'surcharge3')];
$data['$custom_surcharge4'] = ['value' => Number::formatMoney($this->entity->custom_surcharge4, $this->vendor) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'surcharge4')];
$data['$product.item'] = ['value' => '', 'label' => ctrans('texts.item')];
$data['$product.date'] = ['value' => '', 'label' => ctrans('texts.date')];
$data['$product.discount'] = ['value' => '', 'label' => ctrans('texts.discount')];
$data['$product.product_key'] = ['value' => '', 'label' => ctrans('texts.product_key')];
$data['$product.description'] = ['value' => '', 'label' => ctrans('texts.description')];
$data['$product.unit_cost'] = ['value' => '', 'label' => ctrans('texts.unit_cost')];
$data['$product.quantity'] = ['value' => '', 'label' => ctrans('texts.quantity')];
$data['$product.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$product.tax'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$product.tax_name2'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$product.tax_name3'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$product.line_total'] = ['value' => '', 'label' => ctrans('texts.line_total')];
$data['$product.gross_line_total'] = ['value' => '', 'label' => ctrans('texts.gross_line_total')];
$data['$product.description'] = ['value' => '', 'label' => ctrans('texts.description')];
$data['$product.unit_cost'] = ['value' => '', 'label' => ctrans('texts.unit_cost')];
$data['$product.product1'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'product1')];
$data['$product.product2'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'product2')];
$data['$product.product3'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'product3')];
$data['$product.product4'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'product4')];
if ($this->settings->signature_on_pdf) {
$data['$contact.signature'] = ['value' => $this->invitation->signature_base64, 'label' => ctrans('texts.signature')];
} else {
$data['$contact.signature'] = ['value' => '', 'label' => ''];
}
$data['$thanks'] = ['value' => '', 'label' => ctrans('texts.thanks')];
$data['$from'] = ['value' => '', 'label' => ctrans('texts.from')];
$data['$to'] = ['value' => '', 'label' => ctrans('texts.to')];
$data['$details'] = ['value' => '', 'label' => ctrans('texts.details')];
$data['_rate1'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['_rate2'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['_rate3'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$font_size'] = ['value' => $this->settings->font_size . 'px', 'label' => ''];
$data['$font_name'] = ['value' => Helpers::resolveFont($this->settings->primary_font)['name'], 'label' => ''];
$data['$font_url'] = ['value' => Helpers::resolveFont($this->settings->primary_font)['url'], 'label' => ''];
$data['$invoiceninja.whitelabel'] = ['value' => 'https://raw.githubusercontent.com/invoiceninja/invoiceninja/v5-develop/public/images/new_logo.png', 'label' => ''];
$data['$primary_color'] = ['value' => $this->settings->primary_color, 'label' => ''];
$data['$secondary_color'] = ['value' => $this->settings->secondary_color, 'label' => ''];
$data['$item'] = ['value' => '', 'label' => ctrans('texts.item')];
$data['$description'] = ['value' => '', 'label' => ctrans('texts.description')];
$data['$entity_footer'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->footer), $this->company), 'label' => ''];
$data['$footer'] = &$data['$entity_footer'];
$data['$page_size'] = ['value' => $this->settings->page_size, 'label' => ''];
$data['$page_layout'] = ['value' => property_exists($this->settings, 'page_layout') ? $this->settings->page_layout : 'Portrait', 'label' => ''];
$data['$tech_hero_image'] = ['value' => asset('images/pdf-designs/tech-hero-image.jpg'), 'label' => ''];
$data['$autoBill'] = ['value' => ctrans('texts.auto_bill_notification_placeholder'), 'label' => ''];
$data['$auto_bill'] = &$data['$autoBill'];
/*Payment Aliases*/
$data['$paymentLink'] = &$data['$payment_link'];
$data['$payment_url'] = &$data['$payment_link'];
$data['$portalButton'] = &$data['$paymentLink'];
$data['$dir'] = ['value' => optional($this->company->language())->locale === 'ar' ? 'rtl' : 'ltr', 'label' => ''];
$data['$dir_text_align'] = ['value' => optional($this->company->language())->locale === 'ar' ? 'right' : 'left', 'label' => ''];
$data['$payment.date'] = ['value' => '&nbsp;', 'label' => ctrans('texts.payment_date')];
$data['$method'] = ['value' => '&nbsp;', 'label' => ctrans('texts.method')];
$data['$statement_amount'] = ['value' => '', 'label' => ctrans('texts.amount')];
$data['$statement'] = ['value' => '', 'label' => ctrans('texts.statement')];
$data['$entity_images'] = ['value' => $this->generateEntityImagesMarkup(), 'label' => ''];
$data['$payments'] = ['value' => '', 'label' => ctrans('texts.payments')];
$arrKeysLength = array_map('strlen', array_keys($data));
array_multisort($arrKeysLength, SORT_DESC, $data);
return $data;
}
public function makeValues() :array
{
$data = [];
$values = $this->buildEntityDataArray();
foreach ($values as $key => $value) {
$data[$key] = $value['value'];
}
return $data;
}
public function generateLabelsAndValues()
{
$data = [];
$values = $this->buildEntityDataArray();
foreach ($values as $key => $value) {
$data['values'][$key] = $value['value'];
$data['labels'][$key.'_label'] = $value['label'];
}
return $data;
}
private function totalTaxLabels() :string
{
$data = '';
if (! $this->entity_calc->getTotalTaxMap()) {
return $data;
}
foreach ($this->entity_calc->getTotalTaxMap() as $tax) {
$data .= '<span>'.$tax['name'].'</span>';
}
return $data;
}
private function totalTaxValues() :string
{
$data = '';
if (! $this->entity_calc->getTotalTaxMap()) {
return $data;
}
foreach ($this->entity_calc->getTotalTaxMap() as $tax) {
$data .= '<span>'.Number::formatMoney($tax['total'], $this->vendor).'</span>';
}
return $data;
}
private function lineTaxLabels() :string
{
$tax_map = $this->entity_calc->getTaxMap();
$data = '';
foreach ($tax_map as $tax) {
$data .= '<span>'.$tax['name'].'</span>';
}
return $data;
}
private function getCountryName() :string
{
$country = Country::find($this->settings->country_id);
if ($country) {
return ctrans('texts.country_' . $country->name);
}
return '&nbsp;';
}
private function getCountryCode() :string
{
$country = Country::find($this->settings->country_id);
if($country)
return $country->iso_3166_2;
// if ($country) {
// return ctrans('texts.country_' . $country->iso_3166_2);
// }
return '&nbsp;';
}
/**
* Due to the way we are compiling the blade template we
* have no ability to iterate, so in the case
* of line taxes where there are multiple rows,
* we use this function to format a section of rows.
*
* @return string a collection of <tr> rows with line item
* aggregate data
*/
private function makeLineTaxes() :string
{
$tax_map = $this->entity_calc->getTaxMap();
$data = '';
foreach ($tax_map as $tax) {
$data .= '<tr class="line_taxes">';
$data .= '<td>'.$tax['name'].'</td>';
$data .= '<td>'.Number::formatMoney($tax['total'], $this->vendor).'</td></tr>';
}
return $data;
}
private function lineTaxValues() :string
{
$tax_map = $this->entity_calc->getTaxMap();
$data = '';
foreach ($tax_map as $tax) {
$data .= '<span>'.Number::formatMoney($tax['total'], $this->vendor).'</span>';
}
return $data;
}
private function makeTotalTaxes() :string
{
$data = '';
if (! $this->entity_calc->getTotalTaxMap()) {
return $data;
}
foreach ($this->entity_calc->getTotalTaxMap() as $tax) {
$data .= '<tr>';
$data .= '<td colspan="{ count($this->entity->company->settings->pdf_variables->total_columns) - 2 }"></td>';
$data .= '<td>'.$tax['name'].'</td>';
$data .= '<td>'.Number::formatMoney($tax['total'], $this->vendor).'</td></tr>';
}
return $data;
}
private function parseLabelsAndValues($labels, $values, $section) :string
{
$section = strtr($section, $labels);
return strtr($section, $values);
}
/*
| Ensures the URL doesn't have duplicated trailing slash
*/
public function generateAppUrl()
{
//return rtrim(config('ninja.app_url'), "/");
return config('ninja.app_url');
}
/**
* Builds CSS to assist with the generation
* of Repeating headers and footers on the PDF.
* @return string The css string
*/
private function generateCustomCSS() :string
{
$header_and_footer = '
.header, .header-space {
height: 160px;
}
.footer, .footer-space {
height: 160px;
}
.footer {
position: fixed;
bottom: 0;
width: 100%;
}
.header {
position: fixed;
top: 0mm;
width: 100%;
}
@media print {
thead {display: table-header-group;}
tfoot {display: table-footer-group;}
button {display: none;}
body {margin: 0;}
}';
$header = '
.header, .header-space {
height: 160px;
}
.header {
position: fixed;
top: 0mm;
width: 100%;
}
@media print {
thead {display: table-header-group;}
button {display: none;}
body {margin: 0;}
}';
$footer = '
.footer, .footer-space {
height: 160px;
}
.footer {
position: fixed;
bottom: 0;
width: 100%;
}
@media print {
tfoot {display: table-footer-group;}
button {display: none;}
body {margin: 0;}
}';
$css = '';
if ($this->settings->all_pages_header && $this->settings->all_pages_footer) {
$css .= $header_and_footer;
} elseif ($this->settings->all_pages_header && ! $this->settings->all_pages_footer) {
$css .= $header;
} elseif (! $this->settings->all_pages_header && $this->settings->all_pages_footer) {
$css .= $footer;
}
$css .= '
.page {
page-break-after: always;
}
@page {
margin: 0mm
}
html {
';
$css .= 'font-size:'.$this->settings->font_size.'px;';
// $css .= 'font-size:14px;';
$css .= '}';
return $css;
}
/**
* Generate markup for HTML images on entity.
*
* @return string|void
*/
protected function generateEntityImagesMarkup()
{
if ($this->company->getSetting('embed_documents') === false) {
return '';
}
$dom = new \DOMDocument('1.0', 'UTF-8');
$container = $dom->createElement('div');
$container->setAttribute('style', 'display:grid; grid-auto-flow: row; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(2, 1fr);');
foreach ($this->entity->documents as $document) {
if (!$document->isImage()) {
continue;
}
$image = $dom->createElement('img');
$image->setAttribute('src', $document->generateUrl());
$image->setAttribute('style', 'max-height: 100px; margin-top: 20px;');
$container->appendChild($image);
}
$dom->appendChild($container);
return $dom->saveHTML();
}
}

View File

@ -12,10 +12,12 @@ namespace Database\Factories;
use App\DataMapper\CompanySettings;
use App\Models\Company;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Factories\Factory;
class CompanyFactory extends Factory
{
use MakesHash;
/**
* The name of the factory's corresponding model.
*
@ -41,6 +43,7 @@ class CompanyFactory extends Factory
'enabled_modules' => config('ninja.enabled_modules'),
'custom_fields' => (object) [
],
'company_key' => $this->createHash(),
];
}
}

View File

@ -12,6 +12,7 @@ namespace Database\Factories;
use App\Models\Vendor;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class VendorFactory extends Factory
{
@ -45,6 +46,8 @@ class VendorFactory extends Factory
'state' => $this->faker->state,
'postal_code' => $this->faker->postcode,
'country_id' => 4,
'vendor_hash' => Str::random(40),
];
}
}

View File

@ -1,5 +1,6 @@
<?php
use App\Models\Company;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
@ -15,10 +16,10 @@ class CreatePurchaseOrderInvitationsTable extends Migration
{
Schema::create('purchase_order_invitations', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('company_id')->index();
$table->unsignedInteger('company_id');
$table->unsignedInteger('user_id');
$table->unsignedInteger('vendor_contact_id')->unique();
$table->unsignedBigInteger('purchase_order_id')->index()->unique();
$table->unsignedInteger('vendor_contact_id');
$table->unsignedBigInteger('purchase_order_id')->index();
$table->string('key')->index();
$table->string('transaction_reference')->nullable();
$table->string('message_id')->nullable()->index();
@ -37,8 +38,32 @@ class CreatePurchaseOrderInvitationsTable extends Migration
$table->timestamps(6);
$table->softDeletes('deleted_at', 6);
$table->unique(['vendor_contact_id', 'purchase_order_id'], 'vendor_purchase_unique');
$table->index(['deleted_at', 'purchase_order_id', 'company_id'], 'vendor_purchase_company_index');
});
Schema::table('purchase_orders', function (Blueprint $table) {
$table->unsignedInteger('client_id')->nullable()->change();
});
Company::cursor()->each(function ($company){
$settings = $company->settings;
$settings->purchase_order_design_id = 'Wpmbk5ezJn';
$settings->purchase_order_footer = ''; //@implemented
$settings->purchase_order_terms = ''; //@implemented
$settings->purchase_order_public_notes = ''; //@implemented
$settings->purchase_order_number_pattern = ''; //@implemented
$settings->purchase_order_number_counter = 1; //@implemented
$settings->email_subject_purchase_order = '';
$settings->email_template_purchase_order = '';
$company->settings = $settings;
$company->save();
});
}
/**
@ -48,6 +73,5 @@ class CreatePurchaseOrderInvitationsTable extends Migration
*/
public function down()
{
Schema::dropIfExists('purchase_order_invitations');
}
}

View File

@ -4607,7 +4607,9 @@ $LANG = array(
'enable_tooltips_help' => 'Show tooltips when hovering the mouse',
'multiple_client_error' => 'Error: records belong to more than one client',
'login_label' => 'Login to an existing account',
'purchase_order' => 'Purchase Order',
'purchase_order_number' => 'Purchase Order Number',
'purchase_order_number_short' => 'Purchase Order #',
);
return $LANG;

View File

@ -331,6 +331,7 @@
<div class="client-wrapper-left-side">
<h4 class="entity-label">$entity_label</h4>
<div id="client-details" cellspacing="0"></div>
<div id="vendor-details" cellspacing="0"></div>
</div>
<div class="entity-details-wrapper-right-side">
@ -380,7 +381,8 @@ $entity_images
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table'
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table',
'vendor-details', 'client-details'
];
tables.forEach((tableIdentifier) => {

View File

@ -324,6 +324,7 @@
<p class="entity-issued-to">$entity_issued_to_label:</p>
<div class="client-and-entity-wrapper">
<div id="client-details"></div>
<div id="vendor-details"></div>
<div class="entity-details-wrapper">
<table id="entity-details" cellspacing="0" dir="$dir"></table>
</div>
@ -365,6 +366,7 @@ $entity_images
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'vendor-details','client-details'
];
tables.forEach((tableIdentifier) => {

View File

@ -295,6 +295,7 @@
<table id="entity-details" cellspacing="0" dir="$dir"></table>
</div>
<div id="client-details"></div>
<div id="vendor-details"></div>
</div>
<table id="product-table" cellspacing="0" data-ref="table"></table>
<table id="task-table" cellspacing="0" data-ref="table"></table>
@ -333,7 +334,8 @@ $entity_images
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table'
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table',
'client-details','vendor-details'
];
tables.forEach((tableIdentifier) => {

View File

@ -267,6 +267,7 @@
<div id="body">
<div class="header-wrapper">
<div id="client-details"></div>
<div id="vendor-details"></div>
<div class="company-info-wrapper">
<div id="company-details"></div>
<div id="company-address"></div>
@ -322,7 +323,8 @@ $entity_images
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table'
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table',
'vendor-details', 'client-details'
];
tables.forEach((tableIdentifier) => {

View File

@ -279,6 +279,7 @@
<div class="text-with-client">
<h2 class="wrapper-info-text">$to_label</h2>
<div id="client-details"></div>
<div id="vendor-details"></div>
</div>
<div class="company-info">
<h2 class="wrapper-info-text">$from_label</h2>
@ -328,7 +329,8 @@ $entity_images
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table'
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table',
'client-details','vendor-details'
];
tables.forEach((tableIdentifier) => {

View File

@ -297,6 +297,7 @@
<div class="header-right-side-wrapper-left">
<p class="header-text-label">$to_label:</p>
<div id="client-details"></div>
<div id="vendor-details"></div>
</div>
<div class="header-right-side-wrapper-right">
<img
@ -369,7 +370,8 @@ $entity_images
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table'
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table',
'vendor-details','client-details'
];
tables.forEach((tableIdentifier) => {

View File

@ -320,6 +320,7 @@
<div class="logo-client-wrapper">
<img class="company-logo" src="$company.logo" alt="$company.name logo"/>
<div id="client-details"></div>
<div id="vendor-details"></div>
</div>
<div class="table-wrapper">
<table id="product-table" cellspacing="0" data-ref="table"></table>
@ -364,7 +365,8 @@ $entity_images
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table'
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table',
'client-details','vendor-details'
];
tables.forEach((tableIdentifier) => {

View File

@ -270,6 +270,7 @@
</div>
</div>
<div id="client-details"></div>
<div id="vendor-details"></div>
<table id="product-table" cellspacing="0" data-ref="table"></table>
<table id="task-table" cellspacing="0" data-ref="table"></table>
<table id="delivery-note-table" cellspacing="0" data-ref="table"></table>
@ -306,7 +307,8 @@ $entity_images
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table'
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table',
'client-details','vendor-details'
];
tables.forEach((tableIdentifier) => {

View File

@ -349,6 +349,7 @@
<div class="contact-wrapper-right-side">
<p class="contact-label">$to_label:</p>
<div id="client-details"></div>
<div id="vendor-details"></div>
</div>
</div>
<table id="product-table" cellspacing="0" data-ref="table"></table>
@ -409,7 +410,8 @@ $entity_images
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table'
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table',
'client-details','vendor-details'
];
tables.forEach((tableIdentifier) => {

View File

@ -321,6 +321,7 @@
<div class="client-details">
<span class="client-details-to-label">$from_label:</span>
<div id="client-details"></div>
<div id="vendor-details"></div>
</div>
<div class="company-details">
<span class="client-details-to-label">$to_label:</span>
@ -369,7 +370,8 @@ $entity_images
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table'
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table',
'client-details','vendor-details'
];
tables.forEach((tableIdentifier) => {

View File

@ -207,6 +207,8 @@ Route::group(['middleware' => ['throttle:100,1', 'api_db', 'token_auth', 'locale
Route::put('vendors/{vendor}/upload', 'VendorController@upload');
Route::resource('purchase_orders', 'PurchaseOrderController');
Route::post('purchase_orders/bulk', 'PurchaseOrderController@bulk')->name('purchase_orders.bulk');
Route::get('purchase_orders/{purchase_order}/{action}', 'PurchaseOrderController@action')->name('purchase_orders.action');
Route::get('users', 'UserController@index');
Route::get('users/{user}', 'UserController@show')->middleware('password_protected');

View File

@ -78,10 +78,15 @@ class CompanySettingsTest extends TestCase
$this->company->saveSettings($settings, $this->company);
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());
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertStatus(200);

View File

@ -19,6 +19,7 @@ use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
class PurchaseOrderTest extends TestCase
{
@ -40,6 +41,45 @@ class PurchaseOrderTest extends TestCase
}
public function testPostNewPurchaseOrderPdf()
{
$purchase_order = [
'status_id' => 1,
'discount' => 0,
'is_amount_discount' => 1,
'number' => Str::random(10),
'po_number' => Str::random(5),
'due_date' => '2022-01-01',
'date' => '2022-01-01',
'balance' => 100,
'amount' => 100,
'public_notes' => 'notes',
'is_deleted' => 0,
'custom_value1' => 0,
'custom_value2' => 0,
'custom_value3' => 0,
'custom_value4' => 0,
'status' => 1,
'vendor_id' => $this->encodePrimaryKey($this->vendor->id),
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/purchase_orders/', $purchase_order)
->assertStatus(200);
$arr = $response->json();
$purchase_order = PurchaseOrder::find($this->decodePrimaryKey($arr['data']['id']));
$this->assertNotNull($purchase_order);
$x = $purchase_order->service()->markSent()->getPurchaseOrderPdf();
nlog($x);
}
public function testPurchaseOrderRest()
{
$response = $this->withHeaders([
@ -83,7 +123,7 @@ class PurchaseOrderTest extends TestCase
'custom_value3' => 0,
'custom_value4' => 0,
'status' => 1,
'client_id' => $this->encodePrimaryKey($this->client->id),
'vendor_id' => $this->encodePrimaryKey($this->vendor->id),
];
$response = $this->withHeaders([
@ -117,7 +157,7 @@ class PurchaseOrderTest extends TestCase
'custom_value3' => 0,
'custom_value4' => 0,
'status' => 1,
'client_id' => $this->encodePrimaryKey($this->client->id),
'vendor_id' => $this->encodePrimaryKey($this->vendor->id),
];
$response = $this->withHeaders([

View File

@ -13,6 +13,7 @@ namespace Tests\Integration;
use App\Models\Credit;
use App\Models\Design;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Services\PdfMaker\Design as PdfDesignModel;
@ -57,6 +58,8 @@ class HtmlGenerationTest extends TestCase
$entity_design_id = 'quote_design_id';
} elseif ($entity instanceof Credit) {
$entity_design_id = 'credit_design_id';
} elseif ($entity instanceof PurchaseOrder) {
$entity_design_id = 'purchase_order_design_id';
}
$entity_design_id = $entity->design_id ? $entity->design_id : $this->decodePrimaryKey($entity->client->getSetting($entity_design_id));

View File

@ -56,6 +56,7 @@ use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class MockAccountData.
@ -294,6 +295,7 @@ trait MockAccountData
$this->vendor = Vendor::factory()->create([
'user_id' => $user_id,
'company_id' => $this->company->id,
'currency_id' => 1
]);
@ -450,22 +452,12 @@ trait MockAccountData
$this->quote->save();
$this->purchase_order = PurchaseOrderFactory::create($this->company->id, $user_id);
$this->purchase_order->client_id = $this->client->id;
$this->purchase_order->vendor_id = $this->vendor->id;
$this->purchase_order->amount = 10;
$this->purchase_order->balance = 10;
// $this->credit->due_date = now()->addDays(200);
$this->purchase_order->tax_name1 = '';
$this->purchase_order->tax_name2 = '';
$this->purchase_order->tax_name3 = '';