merging template

This commit is contained in:
David Bomba 2023-10-25 13:53:50 +11:00
commit 99b3efda20
83 changed files with 4123 additions and 296 deletions

View File

@ -11,51 +11,52 @@
namespace App\Console\Commands;
use App\DataMapper\ClientRegistrationFields;
use App\DataMapper\CompanySettings;
use App\DataMapper\FeesAndLimits;
use App\Events\Invoice\InvoiceWasCreated;
use App\Events\RecurringInvoice\RecurringInvoiceWasCreated;
use App\Factory\GroupSettingFactory;
use App\Factory\InvoiceFactory;
use App\Factory\InvoiceItemFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Factory\SubscriptionFactory;
use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\BankTransactionRule;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\CompanyToken;
use App\Models\Country;
use App\Models\Credit;
use App\Models\Expense;
use App\Models\Product;
use App\Models\Project;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\Task;
use App\Models\TaxRate;
use App\Models\User;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Repositories\InvoiceRepository;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use stdClass;
use Carbon\Carbon;
use Faker\Factory;
use App\Models\Task;
use App\Models\User;
use App\Utils\Ninja;
use App\Models\Quote;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Vendor;
use App\Models\Account;
use App\Models\Company;
use App\Models\Country;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Product;
use App\Models\Project;
use App\Models\TaxRate;
use App\Libraries\MultiDB;
use App\Models\CompanyToken;
use App\Models\ClientContact;
use App\Models\VendorContact;
use App\Models\CompanyGateway;
use App\Factory\InvoiceFactory;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Utils\Traits\MakesHash;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use App\Models\RecurringInvoice;
use App\DataMapper\FeesAndLimits;
use App\DataMapper\CompanySettings;
use App\Factory\InvoiceItemFactory;
use App\Helpers\Invoice\InvoiceSum;
use App\Models\BankTransactionRule;
use App\Factory\GroupSettingFactory;
use App\Factory\SubscriptionFactory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Cache;
use App\Utils\Traits\GeneratesCounter;
use Illuminate\Support\Facades\Schema;
use stdClass;
use App\Repositories\InvoiceRepository;
use App\Factory\RecurringInvoiceFactory;
use App\Events\Invoice\InvoiceWasCreated;
use App\DataMapper\ClientRegistrationFields;
use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Events\RecurringInvoice\RecurringInvoiceWasCreated;
class CreateSingleAccount extends Command
{
@ -303,6 +304,60 @@ class CreateSingleAccount extends Command
$this->createGateways($company, $user);
$this->createSubsData($company, $user);
$repo = new \App\Repositories\TaskRepository();
Task::query()->cursor()->each(function ($t) use ($repo) {
$repo->save([], $t);
});
$repo = new \App\Repositories\ExpenseRepository();
Expense::query()->cursor()->each(function ($t) use ($repo) {
$repo->save([], $t);
});
$repo = new \App\Repositories\VendorRepository(new \App\Repositories\VendorContactRepository());
Vendor::query()->cursor()->each(function ($t) use ($repo) {
$repo->save([], $t);
});
$repo = new \App\Repositories\ClientRepository(new \App\Repositories\ClientContactRepository());
Client::query()->cursor()->each(function ($t) use ($repo) {
$repo->save([], $t);
});
$repo = new \App\Repositories\RecurringInvoiceRepository();
RecurringInvoice::query()->cursor()->each(function ($t) use ($repo) {
$repo->save([], $t);
});
$repo = new \App\Repositories\InvoiceRepository();
Invoice::query()->cursor()->each(function ($t) use ($repo) {
$repo->save([], $t);
});
$repo = new \App\Repositories\QuoteRepository();
Quote::query()->cursor()->each(function ($t) use ($repo) {
$repo->save([], $t);
});
$repo = new \App\Repositories\CreditRepository();
Credit::query()->cursor()->each(function ($t) use ($repo) {
$repo->save([], $t);
});
Project::query()->cursor()->each(function ($p) {
if(!isset($p->number)) {
$p->number = $this->getNextProjectNumber($p);
$p->save();
}
});
}
private function createSubsData($company, $user)

View File

@ -61,14 +61,14 @@ class ReactBuilder extends Command
foreach (new \RecursiveIteratorIterator($directoryIterator) as $file) {
if ($file->getExtension() == 'js') {
if (str_contains($file->getFileName(), 'index-')) {
$includes .= '<script type="module" crossorigin src="/react/'.$file->getFileName().'"></script>'."\n";
$includes .= '<script type="module" crossorigin src="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'"></script>'."\n";
} else {
$includes .= '<link rel="modulepreload" href="/react/'.$file->getFileName().'">'."\n";
$includes .= '<link rel="modulepreload" href="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'">'."\n";
}
}
if (str_contains($file->getFileName(), '.css')) {
$includes .= '<link rel="stylesheet" href="/react/'.$file->getFileName().'">'."\n";
$includes .= '<link rel="stylesheet" href="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'">'."\n";
}
}

View File

@ -481,9 +481,22 @@ class CompanySettings extends BaseSettings
public $enable_e_invoice = false;
public $classification = ''; // individual, company, partnership, trust, charity, government, other
public $delivery_note_design_id = '';
public $statement_design_id = '';
public $payment_receipt_design_id = '';
public $payment_refund_design_id = '';
public $classification = ''; // individual, business, partnership, trust, charity, government, other
public static $casts = [
'statement_design_id' => 'string',
'delivery_note_design_id' => 'string',
'payment_receipt_design_id' => 'string',
'payment_refund_design_id' => 'string',
'classification' => 'string',
'enable_e_invoice' => 'bool',
'classification' => 'string',
'default_expense_payment_type_id' => 'string',
@ -768,6 +781,8 @@ class CompanySettings extends BaseSettings
'quote_design_id',
'credit_design_id',
'purchase_order_design_id',
'statement_design_id',
'delivery_note_design_id',
];
// /**
@ -999,6 +1014,15 @@ class CompanySettings extends BaseSettings
'$total',
'$credit.balance',
],
'statement_details' => [
'$statement_date',
'$balance'
],
'delivery_note_columns' => [
'$product.item',
'$product.description',
'$product.quantity',
],
];
return json_decode(json_encode($variables));

View File

@ -25,6 +25,8 @@ class DesignFactory
$design->is_active = true;
$design->is_custom = true;
$design->name = '';
$design->is_template = false;
$design->entities = '';
$design->design = new DesignBlocks();
return $design;

View File

@ -63,7 +63,7 @@ class InvoiceItemFactory
$item->line_total = $item->quantity * $item->cost;
$item->is_amount_discount = true;
$item->discount = $faker->numberBetween(1, 10);
$item->notes = $faker->realText(50);
$item->notes = str_replace(['"',"'"],['',""], $faker->realText(20));
$item->product_key = $faker->word();
// $item->custom_value1 = $faker->realText(10);
// $item->custom_value2 = $faker->realText(10);

View File

@ -54,6 +54,19 @@ class DesignFilters extends QueryFilters
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
}
public function entities(string $entities = ''): Builder
{
if (strlen($entities) == 0 || str_contains($entities, ',')) {
return $this->builder;
}
return $this->builder
->where('is_template', true)
->whereRaw('FIND_IN_SET( ? ,entities)', [trim($entities)]);
}
/**
* Filters the query by the users company ID.
*
@ -69,6 +82,17 @@ class DesignFilters extends QueryFilters
});
}
public function template(string $template = 'false'): Builder
{
if (strlen($template) == 0) {
return $this->builder;
}
$bool_val = $template == 'true' ? true : false;
return $this->builder->where('is_template', $bool_val);
}
/**
* Filter the designs by `is_custom` column.
*

View File

@ -43,7 +43,7 @@ class ClientStatementController extends BaseController
}
$pdf = $request->client()->service()->statement(
$request->only(['start_date', 'end_date', 'show_payments_table', 'show_aging_table', 'status', 'show_credits_table']),
$request->only(['start_date', 'end_date', 'show_payments_table', 'show_aging_table', 'status', 'show_credits_table', 'template']),
$send_email
);

View File

@ -301,7 +301,10 @@ class DesignController extends BaseController
*/
public function create(CreateDesignRequest $request)
{
$design = DesignFactory::create(auth()->user()->company()->id, auth()->user()->id);
/** @var \App\Models\User $user */
$user = auth()->user();
$design = DesignFactory::create($user->company()->id, $user->id);
return $this->itemResponse($design);
}
@ -346,7 +349,11 @@ class DesignController extends BaseController
*/
public function store(StoreDesignRequest $request)
{
$design = DesignFactory::create(auth()->user()->company()->id, auth()->user()->id);
/** @var \App\Models\User $user */
$user = auth()->user();
$design = DesignFactory::create($user->company()->id, $user->id);
$design->fill($request->all());
$design->save();

View File

@ -34,6 +34,7 @@ use Illuminate\Support\Facades\Storage;
use App\Transformers\InvoiceTransformer;
use App\Events\Invoice\InvoiceWasCreated;
use App\Events\Invoice\InvoiceWasUpdated;
use App\Services\Template\TemplateAction;
use App\Factory\CloneInvoiceToQuoteFactory;
use App\Http\Requests\Invoice\BulkInvoiceRequest;
use App\Http\Requests\Invoice\EditInvoiceRequest;
@ -537,6 +538,22 @@ class InvoiceController extends BaseController
}, 'print.pdf', ['Content-Type' => 'application/pdf']);
}
if($action == 'template' && $user->can('view', $invoices->first())){
$hash_or_response = $request->boolean('send_email') ? 'email sent' : \Illuminate\Support\Str::uuid();
TemplateAction::dispatch($ids,
$request->template_id,
Invoice::class,
$user->id,
$user->company(),
$user->company()->db,
$hash_or_response,
$request->boolean('send_email'));
return response()->json(['message' => $hash_or_response], 200);
}
/*
* Send the other actions to the switch
*/
@ -718,8 +735,7 @@ class InvoiceController extends BaseController
return response()->json(['message' => 'email sent'], 200);
}
break;
default:
return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400);
}
@ -911,8 +927,12 @@ class InvoiceController extends BaseController
$file = $invoice->service()->getInvoiceDeliveryNote($invoice, $invoice->invitations->first()->contact);
return response()->streamDownload(function () use ($file) {
echo Storage::get($file);
echo $file;
}, basename($file), ['Content-Type' => 'application/pdf']);
// return response()->streamDownload(function () use ($file) {
// echo Storage::get($file);
// }, basename($file), ['Content-Type' => 'application/pdf']);
}
/**

View File

@ -14,7 +14,6 @@ namespace App\Http\Controllers;
use App\Events\Payment\PaymentWasUpdated;
use App\Factory\PaymentFactory;
use App\Filters\PaymentFilters;
use App\Http\Requests\Payment\ActionPaymentRequest;
use App\Http\Requests\Payment\CreatePaymentRequest;
use App\Http\Requests\Payment\DestroyPaymentRequest;
use App\Http\Requests\Payment\EditPaymentRequest;
@ -24,14 +23,12 @@ use App\Http\Requests\Payment\StorePaymentRequest;
use App\Http\Requests\Payment\UpdatePaymentRequest;
use App\Http\Requests\Payment\UploadPaymentRequest;
use App\Models\Account;
use App\Models\Invoice;
use App\Models\Payment;
use App\Repositories\PaymentRepository;
use App\Transformers\PaymentTransformer;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
/**

View File

@ -11,16 +11,31 @@
namespace App\Http\Controllers;
<<<<<<< HEAD
=======
use App\Models\Task;
>>>>>>> support_for_custom_statement_designs
use App\Utils\Ninja;
use App\Models\Quote;
use App\Models\Client;
use App\Models\Credit;
<<<<<<< HEAD
use App\Models\Invoice;
=======
use App\Models\Vendor;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Project;
>>>>>>> support_for_custom_statement_designs
use App\Utils\HtmlEngine;
use App\Libraries\MultiDB;
use App\Factory\QuoteFactory;
use App\Jobs\Util\PreviewPdf;
use App\Models\ClientContact;
<<<<<<< HEAD
=======
use App\Models\PurchaseOrder;
>>>>>>> support_for_custom_statement_designs
use App\Services\Pdf\PdfMock;
use App\Factory\CreditFactory;
use App\Factory\InvoiceFactory;
@ -34,7 +49,10 @@ use Illuminate\Support\Facades\DB;
use App\Services\PdfMaker\PdfMaker;
use Illuminate\Support\Facades\App;
use App\Repositories\QuoteRepository;
<<<<<<< HEAD
use Illuminate\Support\Facades\Cache;
=======
>>>>>>> support_for_custom_statement_designs
use App\Repositories\CreditRepository;
use App\Utils\Traits\MakesInvoiceHtml;
use Turbo124\Beacon\Facades\LightLogs;
@ -43,6 +61,10 @@ use App\Utils\Traits\Pdf\PageNumbering;
use App\Factory\RecurringInvoiceFactory;
use Illuminate\Support\Facades\Response;
use App\DataMapper\Analytics\LivePreview;
<<<<<<< HEAD
=======
use App\Services\Template\TemplateService;
>>>>>>> support_for_custom_statement_designs
use App\Repositories\RecurringInvoiceRepository;
use App\Http\Requests\Preview\DesignPreviewRequest;
use App\Services\PdfMaker\Design as PdfDesignModel;
@ -234,6 +256,9 @@ class PreviewController extends BaseController
*/
public function show()
{
if(request()->has('template'))
return $this->template();
if (request()->has('entity') &&
request()->has('entity_id') &&
! empty(request()->input('entity')) &&
@ -278,6 +303,10 @@ class PreviewController extends BaseController
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $entity_obj->client->company->markdown_enabled,
'options' => [
'client' => $entity_obj->client,
'entity' => $entity_obj,
]
];
$design = new Design(request()->design['name']);
@ -444,6 +473,9 @@ class PreviewController extends BaseController
'options' => [
'all_pages_header' => $entity_obj->client->getSetting('all_pages_header'),
'all_pages_footer' => $entity_obj->client->getSetting('all_pages_footer'),
'client' => $entity_obj->client,
'entity' => $entity_obj,
'variables' => $variables,
],
'process_markdown' => $entity_obj->client->company->markdown_enabled,
];
@ -514,6 +546,80 @@ class PreviewController extends BaseController
return $response;
}
private function template()
{
/** @var \App\Models\User $user */
$user = auth()->user();
/** @var \App\Models\Company $company */
$company = $user->company();
$design_object = json_decode(json_encode(request()->input('design')),1);
// $client_id = Invoice::whereHas('payments')->company()->where('is_deleted', 0)->orderBy('id','desc')->first()->client_id;
// $vendor_id = PurchaseOrder::query()->company()->where('is_deleted', 0)->orderBy('id', 'desc')->first()->vendor_id;
// $data = [
// 'invoices' => Invoice::whereHas('payments')->company()->with('client','payments')->where('client_id', $client_id)->orderBy('id','desc')->take(4)->get(),
// 'quotes' => Quote::query()->company()->with('client')->where('client_id', $client_id)->orderBy('id','desc')->take(4)->get(),
// 'credits' => Credit::query()->company()->with('client')->where('client_id', $client_id)->orderBy('id','desc')->take(4)->get(),
// 'payments' => Payment::query()->company()->with('client')->where('client_id', $client_id)->orderBy('id','desc')->take(4)->get(),
// 'purchase_orders' => PurchaseOrder::query()->company()->with('vendor')->where('vendor_id', $vendor_id)->orderBy('id','desc')->take(5)->get(),
// 'tasks' => Task::query()->company()->with('client','invoice')->where('client_id', $client_id)->orderBy('id','desc')->take(2)->get(),
// 'projects' => Project::query()->company()->with('tasks','client')->where('client_id', $client_id)->orderBy('id','desc')->take(2)->get(),
// ];
$ts = (new TemplateService());
try {
$ts->setCompany($company)
->setTemplate($design_object)
->mock();
}
catch(\Twig\Error\SyntaxError $e)
{
// return response()->json(['message' => 'Twig syntax is invalid.', 'errors' => new \stdClass], 422);
}
$html = $ts->getHtml();
if (request()->query('html') == 'true') {
return $html;
}
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->convertHtmlToPdf($html);
}
if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') {
$pdf = (new NinjaPdf())->build($html);
$numbered_pdf = $this->pageNumbering($pdf, $company);
if ($numbered_pdf) {
$pdf = $numbered_pdf;
}
return $pdf;
}
$file_path = (new PreviewPdf($html, $company))->handle();
$response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf');
return $response;
}
private function stubTemplateData()
{
}
private function blankEntity()
{
@ -554,6 +660,10 @@ class PreviewController extends BaseController
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $invitation->invoice->client->company->markdown_enabled,
'options' => [
'client' => $invitation->invoice->client,
'entity' => $invitation->invoice,
]
];
$maker = new PdfMaker($state);
@ -664,6 +774,10 @@ class PreviewController extends BaseController
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $invoice->client->company->markdown_enabled,
'options' => [
'client' => $invoice->client,
'entity' => $invoice,
]
];
$maker = new PdfMaker($state);

View File

@ -232,6 +232,12 @@ class PreviewPurchaseOrderController extends BaseController
'$product' => $design->design->product,
]),
'variables' => $html->generateLabelsAndValues(),
'options' => [
'client' => null,
'vendor' => $entity_obj->vendor,
'entity' => $entity_obj,
'variables' => $html->generateLabelsAndValues(),
],
'process_markdown' => $entity_obj->company->markdown_enabled,
];

View File

@ -0,0 +1,52 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\BaseController;
use App\Http\Requests\Report\ReportPreviewRequest;
class TemplatePreviewController extends BaseController
{
use MakesHash;
private string $path_prefix = 'templates/';
private string $path_suffix = '.pdf';
public function __construct()
{
parent::__construct();
}
public function __invoke(ReportPreviewRequest $request, ?string $hash)
{
$report = Storage::disk(config('filesystems.default'))->exists($this->path_prefix.$hash.$this->path_suffix);
if(!$report) {
return response()->json(['message' => 'Still working.....'], 409);
}
Cache::forget($hash);
return response()->streamDownload(function () use ($hash) {
echo Storage::get($this->path_prefix.$hash.$this->path_suffix);
Storage::delete($this->path_prefix.$hash.$this->path_suffix);
}, 'template.pdf', ['Content-Type' => 'application/pdf']);
}
}

View File

@ -70,6 +70,11 @@ class PdfSlot extends Component
public function getPdf()
{
if(!$this->invitation){
$this->entity->service()->createInvitations();
$this->invitation = $this->entity->invitations()->first();
}
$blob = [
'entity_type' => $this->resolveEntityType(),
'entity_id' => $this->entity->id,

View File

@ -21,10 +21,9 @@ use Illuminate\Http\Request;
*/
class UserVerified
{
public $user;
public function __construct(?User $user)
public function __construct(public ?User $user)
{
$this->user = property_exists($user, 'id') ? $user : auth()->user();
}

View File

@ -26,13 +26,20 @@ class BulkCompanyGatewayRequest extends Request
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
/** @var \App\Models\User $user */
$user = auth()->user();
return $user->isAdmin();
}
public function rules()
{
/** @var \App\Models\User $user */
$user = auth()->user();
return [
'ids' => ['required','bail','array',Rule::exists('company_gateways', 'id')->where('company_id', auth()->user()->company()->id)],
'ids' => ['required','bail','array',Rule::exists('company_gateways', 'id')->where('company_id', $user->company()->id)],
'action' => 'required|bail|in:archive,restore,delete'
];
}

View File

@ -16,6 +16,11 @@ use App\Models\Account;
class StoreDesignRequest extends Request
{
private array $valid_entities = [
'invoice',
];
/**
* Determine if the user is authorized to make this request.
*
@ -23,20 +28,29 @@ class StoreDesignRequest extends Request
*/
public function authorize() : bool
{
return auth()->user()->isAdmin() && auth()->user()->account->hasFeature(Account::FEATURE_API);
/** @var \App\Models\User $user */
$user = auth()->user();
return $user->isAdmin() && $user->account->hasFeature(Account::FEATURE_API);
;
}
public function rules()
{
/** @var \App\Models\User $user */
$user = auth()->user();
return [
//'name' => 'required',
'name' => 'required|unique:designs,name,null,null,company_id,'.auth()->user()->companyId(),
'name' => 'required|unique:designs,name,null,null,company_id,'.$user->companyId(),
'design' => 'required|array',
'design.header' => 'required|min:1',
'design.body' => 'required|min:1',
'design.footer' => 'required|min:1',
'design.includes' => 'required|min:1',
'design.header' => 'sometimes|string',
'design.body' => 'sometimes|string',
'design.footer' => 'sometimes|string',
'design.includes' => 'sometimes|string',
'is_template' => 'sometimes|boolean',
'entities' => 'sometimes|string|nullable'
];
}
@ -69,6 +83,20 @@ class StoreDesignRequest extends Request
$input['design']['body'] = '';
}
if(array_key_exists('entities', $input)) {
$user_entities = explode(",", $input['entities']);
$e = [];
foreach ($user_entities as $entity) {
if (in_array($entity, $this->valid_entities)) {
$e[] = $entity;
}
}
$input['entities'] = implode(",", $e);
}
$this->replace($input);
}
}

View File

@ -18,6 +18,10 @@ class UpdateDesignRequest extends Request
{
use ChecksEntityStatus;
private array $valid_entities = [
'invoice',
];
/**
* Determine if the user is authorized to make this request.
*
@ -25,12 +29,18 @@ class UpdateDesignRequest extends Request
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
/** @var \App\Models\User $user */
$user = auth()->user();
return $user->isAdmin();
}
public function rules()
{
return [];
return [
'is_template' => 'sometimes|boolean',
'entities' => 'sometimes|string|nullable'
];
}
public function prepareForValidation()
@ -61,6 +71,21 @@ class UpdateDesignRequest extends Request
$input['design']['body'] = '';
}
if(array_key_exists('entities', $input)) {
$user_entities = explode(",", $input['entities']);
$e = [];
foreach ($user_entities as $entity) {
if (in_array($entity, $this->valid_entities)) {
$e[] = $entity;
}
}
$input['entities'] = implode(",", $e);
}
$this->replace($input);
}
}

View File

@ -25,7 +25,10 @@ class BulkInvoiceRequest extends Request
return [
'action' => 'required|string',
'ids' => 'required|array',
'email_type' => 'sometimes|in:reminder1,reminder2,reminder3,reminder_endless,custom1,custom2,custom3,invoice,quote,credit,payment,payment_partial,statement,purchase_order'
'email_type' => 'sometimes|in:reminder1,reminder2,reminder3,reminder_endless,custom1,custom2,custom3,invoice,quote,credit,payment,payment_partial,statement,purchase_order',
'template' => 'sometimes|string',
'template_id' => 'sometimes|string',
'send_email' => 'sometimes|bool'
];
}
}

View File

@ -54,7 +54,7 @@ class StoreInvoiceRequest extends Request
} elseif ($this->file('file')) {
$rules['file'] = $this->file_validation;
}
$rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.$user->company()->id.',is_deleted,0';
$rules['invitations.*.client_contact_id'] = 'distinct';
@ -73,6 +73,8 @@ class StoreInvoiceRequest extends Request
$rules['tax_name2'] = 'bail|sometimes|string|nullable';
$rules['tax_name3'] = 'bail|sometimes|string|nullable';
$rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['partial'] = 'bail|sometimes|nullable|numeric';
$rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date'];
return $rules;
}

View File

@ -76,6 +76,8 @@ class UpdateInvoiceRequest extends Request
$rules['tax_name3'] = 'bail|sometimes|string|nullable';
$rules['status_id'] = 'bail|sometimes|not_in:5'; //do not allow cancelled invoices to be modfified.
$rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['partial'] = 'bail|sometimes|nullable|numeric';
$rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date'];
return $rules;
}

View File

@ -17,9 +17,10 @@ class CreateStatementRequest extends Request
*/
public function authorize(): bool
{
// return auth()->user()->isAdmin();
/** @var \App\Models\User $user */
$user = auth()->user();
return auth()->user()->can('view', $this->client());
return $user->can('view', $this->client());
}
/**
@ -29,14 +30,18 @@ class CreateStatementRequest extends Request
*/
public function rules()
{
/** @var \App\Models\User $user */
$user = auth()->user();
return [
'start_date' => 'required|date_format:Y-m-d',
'end_date' => 'required|date_format:Y-m-d',
'client_id' => 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id,
'client_id' => 'bail|required|exists:clients,id,company_id,'.$user->company()->id,
'show_payments_table' => 'boolean',
'show_aging_table' => 'boolean',
'show_credits_table' => 'boolean',
'status' => 'string',
'template' => 'sometimes|string|nullable',
];
}

View File

@ -170,6 +170,9 @@ class CreateEntityPdf implements ShouldQueue
'options' => [
'all_pages_header' => $this->entity->client->getSetting('all_pages_header'),
'all_pages_footer' => $this->entity->client->getSetting('all_pages_footer'),
'client' => $this->client,
'entity' => $this->entity,
'variables' => $variables,
],
'process_markdown' => $this->entity->client->company->markdown_enabled,
];

View File

@ -23,6 +23,7 @@ use App\Models\QuoteInvitation;
use App\Utils\Traits\MakesHash;
use App\Models\CreditInvitation;
use App\Models\RecurringInvoice;
use App\Services\Pdf\PdfService;
use App\Utils\PhantomJS\Phantom;
use App\Models\InvoiceInvitation;
use App\Utils\HostedPDF\NinjaPdf;
@ -89,6 +90,17 @@ class CreateRawPdf implements ShouldQueue
public function handle()
{
/** Testing this override to improve PDF generation performance */
$ps = new PdfService($this->invitation, 'product', [
'client' => $this->entity->client,
"{$this->entity_string}s" => [$this->entity],
]);
$pdf = $ps->boot()->getPdf();
nlog("pdf timer = ". $ps->execution_time);
/* Forget the singleton*/
App::forgetInstance('translator');
@ -157,6 +169,9 @@ class CreateRawPdf implements ShouldQueue
'options' => [
'all_pages_header' => $this->entity->client->getSetting('all_pages_header'),
'all_pages_footer' => $this->entity->client->getSetting('all_pages_footer'),
'client' => $this->entity->client,
'entity' => $this->entity,
'variables' => $variables,
],
'process_markdown' => $this->entity->client->company->markdown_enabled,
];

View File

@ -31,14 +31,8 @@ class EmailPayment implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $payment;
public $email_builder;
private $contact;
private $company;
public $settings;
/**
@ -49,11 +43,8 @@ class EmailPayment implements ShouldQueue
* @param $contact
* @param $company
*/
public function __construct(Payment $payment, Company $company, ?ClientContact $contact)
public function __construct(public Payment $payment, private Company $company, private ?ClientContact $contact)
{
$this->payment = $payment;
$this->contact = $contact;
$this->company = $company;
$this->settings = $payment->client->getMergedSettings();
}

View File

@ -32,14 +32,8 @@ class EmailRefundPayment implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $payment;
public $email_builder;
private $contact;
private $company;
public $settings;
/**
@ -50,11 +44,8 @@ class EmailRefundPayment implements ShouldQueue
* @param $contact
* @param $company
*/
public function __construct(Payment $payment, Company $company, ClientContact $contact)
public function __construct(public Payment $payment, private Company $company, private ?ClientContact $contact)
{
$this->payment = $payment;
$this->contact = $contact;
$this->company = $company;
$this->settings = $payment->client->getMergedSettings();
}
@ -84,7 +75,9 @@ class EmailRefundPayment implements ShouldQueue
$template_data['body'] = ctrans('texts.refunded_payment').' $payment.refunded <br><br>$invoices';
$template_data['subject'] = ctrans('texts.refunded_payment');
$email_builder = (new PaymentEmailEngine($this->payment, $this->contact, $template_data))->build();
$email_builder = new PaymentEmailEngine($this->payment, $this->contact, $template_data);
$email_builder->is_refund = true;
$email_builder->build();
$invitation = null;

View File

@ -39,9 +39,7 @@ class PreviewReport implements ShouldQueue
/** @var \App\Export\CSV\CreditExport $export */
$export = new $this->report_class($this->company, $this->request);
$report = $export->returnJson();
// nlog($report);
// nlog($this->report_class);
// nlog($report);
Cache::put($this->hash, $report, 60 * 60);
}

View File

@ -154,6 +154,10 @@ class CreatePurchaseOrderPdf implements ShouldQueue
'options' => [
'all_pages_header' => $this->entity->company->getSetting('all_pages_header'),
'all_pages_footer' => $this->entity->company->getSetting('all_pages_footer'),
'client' => null,
'vendor' => $this->vendor,
'entity' => $this->entity,
'variables' => $variables,
],
'process_markdown' => $this->entity->company->markdown_enabled,
];

View File

@ -12,24 +12,14 @@
namespace App\Mail;
use App\Models\Company;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
class DownloadDocuments extends Mailable
{
// use Queueable, SerializesModels;
public $file_path;
public $company;
public function __construct($file_path, Company $company)
public function __construct(public string $file_path, public Company $company)
{
$this->file_path = $file_path;
$this->company = $company;
}
/**

View File

@ -15,12 +15,14 @@ use App\Utils\Ninja;
use App\Utils\Number;
use App\Utils\Helpers;
use App\Models\Account;
use App\Models\Payment;
use App\Utils\Traits\MakesDates;
use App\Jobs\Entity\CreateRawPdf;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Storage;
use App\DataMapper\EmailTemplateDefaults;
use App\Services\Template\TemplateAction;
class PaymentEmailEngine extends BaseEmailEngine
{
@ -44,6 +46,8 @@ class PaymentEmailEngine extends BaseEmailEngine
private $payment_template_subject;
public bool $is_refund = false;
public function __construct($payment, $contact, $template_data = null)
{
$this->payment = $payment;
@ -91,10 +95,53 @@ class PaymentEmailEngine extends BaseEmailEngine
->setViewText('');
if ($this->client->getSetting('pdf_email_attachment') !== false && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
$this->payment->invoices->each(function ($invoice) {
$pdf = ((new CreateRawPdf($invoice->invitations->first(), $invoice->company->db))->handle());
$this->setAttachments([['file' => base64_encode($pdf), 'name' => $invoice->numberFormatter().'.pdf']]);
$template_in_use = false;
if($this->is_refund && strlen($this->payment->client->getSetting('payment_refund_design_id')) > 2) {
$pdf = (new TemplateAction(
[$this->payment->hashed_id],
$this->payment->client->getSetting('payment_refund_design_id'),
Payment::class,
$this->payment->user_id,
$this->payment->company,
$this->payment->company->db,
'nohash',
false
))->handle();
$file_name = ctrans('texts.payment_refund_receipt', ['number' => $this->payment->number ]) . '.pdf';
$file_name = str_replace(' ', '_', $file_name);
$this->setAttachments([['file' => base64_encode($pdf), 'name' => $file_name]]);
$template_in_use = true;
} elseif(!$this->is_refund && strlen($this->payment->client->getSetting('payment_receipt_design_id')) > 2) {
$pdf = (new TemplateAction(
[$this->payment->hashed_id],
$this->payment->client->getSetting('payment_receipt_design_id'),
Payment::class,
$this->payment->user_id,
$this->payment->company,
$this->payment->company->db,
'nohash',
false
))->handle();
$file_name = ctrans('texts.payment_receipt', ['number' => $this->payment->number ]) . '.pdf';
$file_name = str_replace(' ', '_', $file_name);
$this->setAttachments([['file' => base64_encode($pdf), 'name' => $file_name]]);
$template_in_use = true;
}
$this->payment->invoices->each(function ($invoice) use($template_in_use){
if(!$template_in_use)
{
$pdf = ((new CreateRawPdf($invoice->invitations->first(), $invoice->company->db))->handle());
$file_name = $invoice->numberFormatter().'.pdf';
$this->setAttachments([['file' => base64_encode($pdf), 'name' => $file_name]]);
}
//attach invoice documents also to payments
if ($this->client->getSetting('document_email_attachment') !== false) {

View File

@ -11,6 +11,7 @@
namespace App\Models;
use App\Services\Template\TemplateService;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@ -29,7 +30,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int|null $updated_at
* @property int|null $deleted_at
* @property-read \App\Models\Company|null $company
* @property-read mixed $hashed_id
* @property-read string $hashed_id
* @method static \Illuminate\Database\Eloquent\Builder|BaseModel company()
* @method static \Illuminate\Database\Eloquent\Builder|BaseModel exclude($columns)
* @method static \Illuminate\Database\Eloquent\Builder|Design filter(\App\Filters\QueryFilters $filters)
@ -69,10 +70,17 @@ class Design extends BaseModel
'name',
'design',
'is_active',
'is_template',
'entities',
];
public function company()
{
return $this->belongsTo(Company::class);
}
public function service(): TemplateService
{
return (new TemplateService($this))->setCompany($this->company);
}
}

View File

@ -30,6 +30,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int $id
* @property int $company_id
* @property int $client_id
* @property int $category_id
* @property int|null $project_id
* @property int|null $vendor_id
* @property int|null $user_id
@ -58,6 +59,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int|null $exchange_currency_id
* @property \App\Models\Paymentable $paymentable
* @property object|null $meta
* @property object|null $refund_meta
* @property string|null $custom_value1
* @property string|null $custom_value2
* @property string|null $custom_value3
@ -151,12 +153,12 @@ class Payment extends BaseModel
'number',
'exchange_currency_id',
'exchange_rate',
// 'is_manual',
'private_notes',
'custom_value1',
'custom_value2',
'custom_value3',
'custom_value4',
'category_id',
];
protected $casts = [
@ -167,6 +169,7 @@ class Payment extends BaseModel
'deleted_at' => 'timestamp',
'is_deleted' => 'bool',
'meta' => 'object',
'refund_meta' => 'array',
];
protected $with = [
@ -454,11 +457,6 @@ class Payment extends BaseModel
public function getLink() :string
{
// if (Ninja::isHosted()) {
// $domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain();
// } else {
// $domain = config('ninja.app_url');
// }
if (Ninja::isHosted()) {
$domain = $this->company->domain();
@ -494,4 +492,11 @@ class Payment extends BaseModel
return $use_react_url ? config('ninja.react_url')."/#/payments/{$this->hashed_id}/edit" : config('ninja.app_url');
}
public function setRefundMeta(array $data)
{
$tmp_meta = $this->refund_meta ?? [];
$tmp_meta[] = $data;
$this->refund_meta = $tmp_meta;
}
}

View File

@ -53,7 +53,7 @@ class PayFastPaymentDriver extends BaseDriver
if ($this->client->currency()->code == 'ZAR') {
$types[] = GatewayType::CREDIT_CARD;
}
return $types;
}

View File

@ -162,6 +162,8 @@ class ActivityRepository extends BaseRepository
'options' => [
'all_pages_header' => $entity->vendor->getSetting('all_pages_header'),
'all_pages_footer' => $entity->vendor->getSetting('all_pages_footer'),
'vendor' => $entity->vendor,
'entity' => $entity,
],
'process_markdown' => $entity->vendor->company->markdown_enabled,
];
@ -230,6 +232,8 @@ class ActivityRepository extends BaseRepository
'options' => [
'all_pages_header' => $entity->client->getSetting('all_pages_header'),
'all_pages_footer' => $entity->client->getSetting('all_pages_footer'),
'client' => $entity->client,
'entity' => $entity,
],
'process_markdown' => $entity->client->company->markdown_enabled,
];

View File

@ -142,11 +142,8 @@ class PaymentRepository extends BaseRepository
$invoices = Invoice::withTrashed()->whereIn('id', array_column($data['invoices'], 'invoice_id'))->get();
// $payment->invoices()->saveMany($invoices); //25-06-2023
//todo optimize this into a single query
foreach ($data['invoices'] as $paid_invoice) {
// $invoice = Invoice::withTrashed()->whereId($paid_invoice['invoice_id'])->first();
$invoice = $invoices->firstWhere('id', $paid_invoice['invoice_id']);
if ($invoice) {

View File

@ -17,7 +17,7 @@ use App\Utils\Traits\GeneratesCounter;
use Illuminate\Database\QueryException;
/**
* TaskRepository.
* App\Repositories\TaskRepository.
*/
class TaskRepository extends BaseRepository
{

View File

@ -78,6 +78,19 @@ class PaymentMethod
->sortby(function ($model) use ($transformed_ids) { //company gateways are sorted in order of priority
return array_search($model->id, $transformed_ids); // this closure sorts for us
});
if($this->gateways->count() == 0 && count($transformed_ids) >=1) {
/** This is a fallback in case a user archives some gateways that have been ordered preferentially. */
$this->gateways = CompanyGateway::query()
->with('gateway')
->where('company_id', $this->client->company_id)
->where('gateway_key', '!=', '54faab2ab6e3223dbe848b1686490baa')
->whereNull('deleted_at')
->where('is_deleted', false)->get();
}
} else {
$this->gateways = CompanyGateway::query()
->with('gateway')

View File

@ -12,27 +12,32 @@
namespace App\Services\Client;
use App\Factory\InvoiceFactory;
use App\Factory\InvoiceInvitationFactory;
use App\Factory\InvoiceItemFactory;
use App\Utils\Number;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Design;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\PdfMaker;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine;
use App\Utils\Number;
use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\Pdf\PdfMaker as PdfMakerTrait;
use Illuminate\Support\Carbon;
use App\Factory\InvoiceFactory;
use App\Utils\Traits\MakesHash;
use App\Utils\PhantomJS\Phantom;
use App\Utils\HostedPDF\NinjaPdf;
use Illuminate\Support\Facades\DB;
use App\Models\Credit;
use App\Factory\InvoiceItemFactory;
use App\Services\PdfMaker\PdfMaker;
use App\Factory\InvoiceInvitationFactory;
use Illuminate\Database\Eloquent\Builder;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\Pdf\PdfMaker as PdfMakerTrait;
class Statement
{
use PdfMakerTrait;
use MakesHash;
use MakesDates;
/**
* @var Invoice|Payment|null
@ -53,6 +58,20 @@ class Statement
$html = new HtmlEngine($this->getInvitation());
$variables = [];
if($this->client->getSetting('statement_design_id') != '') {
$variables['values']['$start_date'] = $this->translateDate($this->options['start_date'], $this->client->date_format(), $this->client->locale());
$variables['values']['$end_date'] = $this->translateDate($this->options['end_date'], $this->client->date_format(), $this->client->locale());
$variables['labels']['$start_date_label'] = ctrans('texts.start_date');
$variables['labels']['$end_date_label'] = ctrans('texts.end_date');
return $this->templateStatement($variables);
}
$variables = $html->generateLabelsAndValues();
if ($this->getDesign()->is_custom) {
$this->options['custom_partials'] = \json_decode(\json_encode($this->getDesign()->design), true);
@ -71,16 +90,26 @@ class Statement
'pdf_variables' => (array) $this->entity->company->settings->pdf_variables,
'$product' => $this->getDesign()->design->product,
'variables' => $variables,
'invoices' => $this->getInvoices(),
'payments' => $this->getPayments(),
'credits' => $this->getCredits(),
'invoices' => $this->getInvoices()->cursor(),
'payments' => $this->getPayments()->cursor(),
'credits' => $this->getCredits()->cursor(),
'aging' => $this->getAging(),
], \App\Services\PdfMaker\Design::STATEMENT),
'variables' => $variables,
'options' => [],
'options' => [
'client' => $this->client,
'entity' => $this->entity,
'variables' => $variables,
'invoices' => $this->getInvoices()->cursor(),
'payments' => $this->getPayments()->cursor(),
'credits' => $this->getCredits()->cursor(),
'aging' => $this->getAging(),
],
'process_markdown' => $this->entity->client->company->markdown_enabled,
];
$maker = new PdfMaker($state);
$maker
@ -88,29 +117,64 @@ class Statement
->build();
$pdf = null;
try {
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
$pdf = (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
} elseif (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') {
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
} else {
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
}
} catch (\Exception $e) {
nlog(print_r($e->getMessage(), 1));
}
$html = $maker->getCompiledHTML(true);
if ($this->rollback) {
\DB::connection(config('database.default'))->rollBack();
}
$pdf = $this->convertToPdf($html);
$maker = null;
$state = null;
return $pdf;
}
private function templateStatement($variables)
{
if(isset($this->options['template']))
$statement_design_id = $this->options['template'];
else
$statement_design_id = $this->client->getSetting('statement_design_id');
$template = Design::where('id', $this->decodePrimaryKey($statement_design_id))
->where('company_id', $this->client->company_id)
->first();
$ts = $template->service()->build([
'variables' => collect([$variables]),
'invoices' => $this->getInvoices()->get(),
'payments' => $this->options['show_payments_table'] ? $this->getPayments()->get() : collect([]),
'credits' => $this->options['show_credits_table'] ? $this->getCredits()->get() : collect([]),
'aging' => $this->options['show_aging_table'] ? $this->getAging() : collect([]),
]);
$html = $ts->getHtml();
return $this->convertToPdf($html);
}
private function convertToPdf(string $html): mixed
{
$pdf = false;
try {
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
$pdf = (new Phantom)->convertHtmlToPdf($html);
} elseif (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') {
$pdf = (new NinjaPdf())->build($html);
} else {
$pdf = $this->makePdf(null, null, $html);
}
} catch (\Exception $e) {
nlog(print_r($e->getMessage(), 1));
}
return $pdf;
}
/**
* Setup correct entity instance.
*
@ -222,9 +286,9 @@ class Statement
/**
* The collection of invoices for the statement.
*
* @return Invoice[]|\Illuminate\Support\LazyCollection
* @return Builder
*/
public function getInvoices(): \Illuminate\Support\LazyCollection
public function getInvoices(): Builder
{
return Invoice::withTrashed()
->with('payments.type')
@ -234,8 +298,7 @@ class Statement
->whereIn('status_id', $this->invoiceStatuses())
->whereBetween('date', [Carbon::parse($this->options['start_date']), Carbon::parse($this->options['end_date'])])
->orderBy('due_date', 'ASC')
->orderBy('date', 'ASC')
->cursor();
->orderBy('date', 'ASC');
}
private function invoiceStatuses() :array
@ -256,7 +319,6 @@ class Statement
case 'unpaid':
return [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL];
default:
return [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID];
@ -266,9 +328,9 @@ class Statement
/**
* The collection of payments for the statement.
*
* @return Payment[]|\Illuminate\Support\LazyCollection
* @return Builder
*/
protected function getPayments(): \Illuminate\Support\LazyCollection
protected function getPayments(): Builder
{
return Payment::withTrashed()
->with('client.country', 'invoices')
@ -277,19 +339,18 @@ class Statement
->where('client_id', $this->client->id)
->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
->whereBetween('date', [Carbon::parse($this->options['start_date']), Carbon::parse($this->options['end_date'])])
->orderBy('date', 'ASC')
->cursor();
->orderBy('date', 'ASC');
}
/**
* The collection of credits for the statement.
*
* @return Credit[]|\Illuminate\Support\LazyCollection
* @return Builder
*/
protected function getCredits(): \Illuminate\Support\LazyCollection
protected function getCredits(): Builder
{
return Credit::withTrashed()
->with('client.country', 'invoices')
->with('client.country','invoice')
->where('is_deleted', false)
->where('company_id', $this->client->company_id)
->where('client_id', $this->client->id)
@ -299,8 +360,7 @@ class Statement
$query->whereDate('due_date', '>=', $this->options['end_date'])
->orWhereNull('due_date');
})
->orderBy('date', 'ASC')
->cursor();
->orderBy('date', 'ASC');
}
/**
@ -339,7 +399,7 @@ class Statement
* @param mixed $range
* @return string
*/
private function getAgingAmount($range)
private function getAgingAmount($range): string
{
$ranges = $this->calculateDateRanges($range);

View File

@ -0,0 +1,671 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Email;
use App\DataMapper\Analytics\EmailFailure;
use App\DataMapper\Analytics\EmailSuccess;
use App\Events\Invoice\InvoiceWasEmailedAndFailed;
use App\Events\Payment\PaymentWasEmailedAndFailed;
use App\Jobs\Util\SystemLogger;
use App\Libraries\Google\Google;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Models\User;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use App\Utils\VendorHtmlEngine;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use Turbo124\Beacon\Facades\LightLogs;
class AdminEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
public $tries = 4;
public $deleteWhenMissingModels = true;
public bool $override;
protected ?string $client_postmark_secret = null;
protected ?string $client_mailgun_secret = null;
protected ?string $client_mailgun_domain = null;
protected ?string $client_mailgun_endpoint = null;
private string $mailer = 'default';
public Mailable $mailable;
public function __construct(public EmailObject $email_object, public Company $company)
{
}
/**
* The backoff time between retries.
*
* @return array
*/
public function backoff()
{
return [rand(10, 20), rand(30, 45), rand(60, 79), rand(160, 400)];
}
public function handle()
{
MultiDB::setDb($this->company->db);
$this->setOverride()
->buildMailable();
if ($this->preFlightChecksFail()) {
return;
}
$this->email();
}
/**
* Sets the override flag
*
* @return self
*/
public function setOverride(): self
{
$this->override = $this->email_object->override;
return $this;
}
/**
* Populates the mailable
*
* @return self
*/
public function buildMailable(): self
{
$this->mailable = new AdminEmailMailable($this->email_object);
return $this;
}
/**
* Attempts to send the email
*
* @return void
*/
public function email()
{
$this->setMailDriver();
/* Init the mailer*/
$mailer = Mail::mailer($this->mailer);
/* Additional configuration if using a client third party mailer */
if ($this->client_postmark_secret) {
$mailer->postmark_config($this->client_postmark_secret);
}
if ($this->client_mailgun_secret) {
$mailer->mailgun_config($this->client_mailgun_secret, $this->client_mailgun_domain, $this->client_mailgun_endpoint);
}
/* Attempt the send! */
try {
nlog("Using mailer => ". $this->mailer. " ". now()->toDateTimeString());
$mailer->send($this->mailable);
Cache::increment("email_quota".$this->company->account->key);
LightLogs::create(new EmailSuccess($this->company->company_key))
->send();
} catch(\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
} catch(\Symfony\Component\Mime\Exception\LogicException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
} catch (\Exception | \RuntimeException | \Google\Service\Exception $e) {
nlog("Mailer failed with {$e->getMessage()}");
$message = $e->getMessage();
if (stripos($e->getMessage(), 'code 406') || stripos($e->getMessage(), 'code 300') || stripos($e->getMessage(), 'code 413')) {
$message = "Either Attachment too large, or recipient has been suppressed.";
$this->fail();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
$this->cleanUpMailers();
return;
}
/**
* Post mark buries the proper message in a a guzzle response
* this merges a text string with a json object
* need to harvest the ->Message property using the following
*/
if ($e instanceof ClientException) { //postmark specific failure
$response = $e->getResponse();
$message_body = json_decode($response->getBody()->getContents());
if ($message_body && property_exists($message_body, 'Message')) {
$message = $message_body->Message;
nlog($message);
}
$this->fail();
$this->cleanUpMailers();
return;
}
//only report once, not on all tries
if ($this->attempts() == $this->tries) {
/* If the is an entity attached to the message send a failure mailer */
$this->entityEmailFailed($message);
/* Don't send postmark failures to Sentry */
if (Ninja::isHosted() && (!$e instanceof ClientException)) {
app('sentry')->captureException($e);
}
}
sleep(rand(0, 3));
$this->release($this->backoff()[$this->attempts()-1]);
$message = null;
}
$this->cleanUpMailers();
}
/**
* On the hosted platform we scan all outbound email for
* spam. This sequence processes the filters we use on all
* emails.
*
* @return bool
*/
public function preFlightChecksFail(): bool
{
/* Always send if disabled */
if($this->override) {
return false;
}
/* If we are migrating data we don't want to fire any emails */
if ($this->company->is_disabled) {
return true;
}
if (Ninja::isSelfHost()) {
return false;
}
/* To handle spam users we drop all emails from flagged accounts */
if ($this->company->account && $this->company->account->is_flagged) {
return true;
}
/* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */
if ($this->hasInValidEmails()) {
return true;
}
/* GMail users are uncapped */
if (in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun'])) {
return false;
}
/* On the hosted platform, if the user is over the email quotas, we do not send the email. */
if ($this->company->account && $this->company->account->emailQuotaExceeded()) {
return true;
}
/* If the account is verified, we allow emails to flow */
if ($this->company->account && $this->company->account->is_verified_account) {
//11-01-2022
/* Continue to analyse verified accounts in case they later start sending poor quality emails*/
// if(class_exists(\Modules\Admin\Jobs\Account\EmailQuality::class))
// (new \Modules\Admin\Jobs\Account\EmailQuality($this->nmo, $this->company))->run();
return false;
}
/* On the hosted platform if the user has not verified their account we fail here - but still check what they are trying to send! */
if ($this->company->account && !$this->company->account->account_sms_verified) {
if (class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class)) {
return (new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->company))->run();
}
return true;
}
/* On the hosted platform we actively scan all outbound emails to ensure outbound email quality remains high */
if (class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class)) {
return (new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->company))->run();
}
return false;
}
/**
* hasInValidEmails
*
* @return bool
*/
private function hasInValidEmails(): bool
{
foreach ($this->email_object->to as $address_object) {
if (strpos($address_object->address, '@example.com') !== false) {
return true;
}
if (!str_contains($address_object->address, "@")) {
return true;
}
if ($address_object->address == " ") {
return true;
}
}
return false;
}
/**
* Sets the mail driver to use and applies any specific configuration
* the the mailable
*/
private function setMailDriver(): self
{
switch ($this->email_object->settings->email_sending_method) {
case 'default':
$this->mailer = config('mail.default');
break;
case 'gmail':
$this->mailer = 'gmail';
$this->setGmailMailer();
return $this;
case 'office365':
case 'microsoft':
$this->mailer = 'office365';
$this->setOfficeMailer();
return $this;
case 'client_postmark':
$this->mailer = 'postmark';
$this->setPostmarkMailer();
return $this;
case 'client_mailgun':
$this->mailer = 'mailgun';
$this->setMailgunMailer();
return $this;
default:
$this->mailer = config('mail.default');
return $this;
}
if (Ninja::isSelfHost()) {
$this->setSelfHostMultiMailer();
}
return $this;
}
/**
* Allows configuration of multiple mailers
* per company for use by self hosted users
*/
private function setSelfHostMultiMailer(): void
{
if (env($this->company->id . '_MAIL_HOST')) {
config([
'mail.mailers.smtp' => [
'transport' => 'smtp',
'host' => env($this->company->id . '_MAIL_HOST'),
'port' => env($this->company->id . '_MAIL_PORT'),
'username' => env($this->company->id . '_MAIL_USERNAME'),
'password' => env($this->company->id . '_MAIL_PASSWORD'),
],
]);
if (env($this->company->id . '_MAIL_FROM_ADDRESS')) {
$this->mailable
->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME')));
}
}
}
/**
* Ensure we discard any data that is not required
*
* @return void
*/
private function cleanUpMailers(): void
{
$this->client_postmark_secret = null;
$this->client_mailgun_secret = null;
$this->client_mailgun_domain = null;
$this->client_mailgun_endpoint = null;
//always dump the drivers to prevent reuse
app('mail.manager')->forgetMailers();
}
/**
* Check to ensure no cross account
* emails can be sent.
*
* @param User $user
*/
private function checkValidSendingUser($user)
{
/* Always ensure the user is set on the correct account */
if ($user->account_id != $this->company->account_id) {
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
}
/**
* Resolves the sending user
* when configuring the Mailer
* on behalf of the client
*
* @return User $user
*/
private function resolveSendingUser(): ?User
{
$sending_user = $this->email_object->settings->gmail_sending_user_id;
if ($sending_user == "0") {
$user = $this->company->owner();
} else {
$user = User::find($this->decodePrimaryKey($sending_user));
}
return $user;
}
/**
* Configures Mailgun using client supplied secret
* as the Mailer
*/
private function setMailgunMailer()
{
if (strlen($this->email_object->settings->mailgun_secret) > 2 && strlen($this->email_object->settings->mailgun_domain) > 2) {
$this->client_mailgun_secret = $this->email_object->settings->mailgun_secret;
$this->client_mailgun_domain = $this->email_object->settings->mailgun_domain;
$this->client_mailgun_endpoint = $this->email_object->settings->mailgun_endpoint;
} else {
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
$user = $this->resolveSendingUser();
$sending_email = (isset($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email;
$sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name();
$this->mailable
->from($sending_email, $sending_user);
}
/**
* Configures Postmark using client supplied secret
* as the Mailer
*/
private function setPostmarkMailer()
{
if (strlen($this->email_object->settings->postmark_secret) > 2) {
$this->client_postmark_secret = $this->email_object->settings->postmark_secret;
} else {
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
$user = $this->resolveSendingUser();
$sending_email = (isset($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email;
$sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name();
$this->mailable
->from($sending_email, $sending_user);
}
/**
* Configures Microsoft via Oauth
* as the Mailer
*/
private function setOfficeMailer()
{
$user = $this->resolveSendingUser();
$this->checkValidSendingUser($user);
nlog("Sending via {$user->name()}");
$token = $this->refreshOfficeToken($user);
if ($token) {
$user->oauth_user_token = $token;
$user->save();
} else {
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
$this->mailable
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
}
/**
* Configures GMail via Oauth
* as the Mailer
*/
private function setGmailMailer()
{
$user = $this->resolveSendingUser();
$this->checkValidSendingUser($user);
nlog("Sending via {$user->name()}");
$google = (new Google())->init();
try {
if ($google->getClient()->isAccessTokenExpired()) {
$google->refreshToken($user);
$user = $user->fresh();
}
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
} catch(\Exception $e) {
$this->logMailError('Gmail Token Invalid', $this->company->clients()->first());
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
/**
* If the user doesn't have a valid token, notify them
*/
if (!$user->oauth_user_token) {
$this->company->account->gmailCredentialNotification();
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
/*
* Now that our token is refreshed and valid we can boot the
* mail driver at runtime and also set the token which will persist
* just for this request.
*/
$token = $user->oauth_user_token->access_token;
if (!$token) {
$this->company->account->gmailCredentialNotification();
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
$this->mailable
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
}
/**
* Logs any errors to the SystemLog
*
* @param string $errors
* @param null | \App\Models\Client $recipient_object
* @return void
*/
private function logMailError($errors, $recipient_object) :void
{
(new SystemLogger(
$errors,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND,
SystemLog::TYPE_FAILURE,
$recipient_object,
$this->company
))->handle();
$job_failure = new EmailFailure($this->company->company_key);
$job_failure->string_metric5 = 'failed_email';
$job_failure->string_metric6 = substr($errors, 0, 150);
LightLogs::create($job_failure)
->send();
$job_failure = null;
}
/**
* Attempts to refresh the Microsoft refreshToken
*
* @param \App\Models\User $user
* @return mixed
*/
private function refreshOfficeToken(User $user): mixed
{
$expiry = $user->oauth_user_token_expiry ?: now()->subDay();
if ($expiry->lt(now())) {
$guzzle = new \GuzzleHttp\Client();
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
$token = json_decode($guzzle->post($url, [
'form_params' => [
'client_id' => config('ninja.o365.client_id') ,
'client_secret' => config('ninja.o365.client_secret') ,
'scope' => 'email Mail.Send offline_access profile User.Read openid',
'grant_type' => 'refresh_token',
'refresh_token' => $user->oauth_user_refresh_token
],
])->getBody()->getContents());
if ($token) {
$user->oauth_user_refresh_token = property_exists($token, 'refresh_token') ? $token->refresh_token : $user->oauth_user_refresh_token;
$user->oauth_user_token = $token->access_token;
$user->oauth_user_token_expiry = now()->addSeconds($token->expires_in);
$user->save();
return $token->access_token;
}
return false;
}
return $user->oauth_user_token;
}
/**
* Entity notification when an email fails to send
*
* @param string $message
* @return void
*/
private function entityEmailFailed($message): void
{
$class = get_class($this->email_object->entity);
switch ($class) {
case Invoice::class:
event(new InvoiceWasEmailedAndFailed($this->email_object->invitation, $this->company, $message, $this->email_object->html_template, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
break;
case Payment::class:
event(new PaymentWasEmailedAndFailed($this->email_object->entity, $this->company, $message, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
break;
default:
# code...
break;
}
if ($this->email_object->client) {
$this->logMailError($message, $this->email_object->client);
}
}
public function failed($exception = null)
{
if ($exception) {
nlog($exception->getMessage());
}
config(['queue.failed.driver' => null]);
}
}

View File

@ -0,0 +1,106 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Email;
use App\Models\Document;
use Illuminate\Mail\Attachment;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Mail\Mailables\Headers;
use Illuminate\Support\Facades\URL;
class AdminEmailMailable extends Mailable
{
public int $max_attachment_size = 3000000;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(public EmailObject $email_object)
{
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: str_replace("<br>", "", $this->email_object->subject),
tags: [$this->email_object->company_key],
replyTo: $this->email_object->reply_to,
from: $this->email_object->from,
to: $this->email_object->to,
bcc: $this->email_object->bcc,
cc: $this->email_object->cc,
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'email.admin.generic',
text: 'email.admin.generic_text',
with: [
'title' => $this->email_object->subject,
'message' => $this->email_object->body,
'url' => $this->email_object->url ?? null,
'button' => $this->email_object->button ?? null,
'signature' => $this->email_object->company->owner()->signature,
'logo' => $this->email_object->company->present()->logo(),
'settings' => $this->email_object->settings,
'whitelabel' => $this->email_object->company->account->isPaid() ? true : false,
]
);
}
/**
* Get the attachments for the message.
*
* @return array
*/
public function attachments()
{
$attachments = [];
$attachments = collect($this->email_object->attachments)->map(function ($file) {
return Attachment::fromData(fn () => base64_decode($file['file']), $file['name']);
});
return $attachments->toArray();
}
/**
* Get the message headers.
*
* @return \Illuminate\Mail\Mailables\Headers
*/
public function headers()
{
return new Headers(
messageId: null,
references: [],
text: $this->email_object->headers,
);
}
}

View File

@ -75,7 +75,6 @@ class Email implements ShouldQueue
*/
public function backoff()
{
// return [10, 30, 60, 240];
return [rand(10, 20), rand(30, 45), rand(60, 79), rand(160, 400)];
}

View File

@ -121,4 +121,8 @@ class EmailObject
public ?string $template = null; //invoice //quote //reminder1
public array $links = [];
public ?string $button = null;
public ?string $url = null;
}

View File

@ -49,6 +49,9 @@ class ApplyPayment extends AbstractService
$this->invoice->service()->clearPartial()->setDueDate()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($amount_paid)->updatePaidToDate($amount_paid*-1)->save();
}
$this->invoice->service()->checkReminderStatus()->save();
} else {
if ($this->payment_amount == $this->invoice->balance) {
$amount_paid = $this->payment_amount * -1;

View File

@ -65,15 +65,27 @@ class ApplyPaymentAmount extends AbstractService
'amount' => $payment->amount,
]);
$this->invoice->next_send_date = null;
$this->invoice->service()
$has_partial = $this->invoice->hasPartial();
$invoice_service = $this->invoice->service()
->setExchangeRate()
->updateBalance($payment->amount * -1)
->updatePaidToDate($payment->amount)
->setCalculatedStatus()
->applyNumber()
->save();
->applyNumber();
if ($has_partial) {
$this->invoice->partial = max(0, $this->invoice->partial - $payment->amount);
$invoice_service->checkReminderStatus();
}
if($this->invoice->balance == 0){
$this->invoice->next_send_date = null;
}
$this->invoice = $invoice_service->save();
$this->invoice
->client

View File

@ -17,6 +17,7 @@ use App\Models\Design;
use App\Models\Invoice;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\PdfMaker as PdfMakerService;
use App\Services\Template\TemplateService;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine;
use App\Utils\PhantomJS\Phantom;
@ -40,6 +41,22 @@ class GenerateDeliveryNote
public function run()
{
$delivery_note_design_id = $this->invoice->client->getSetting('delivery_note_design_id');
$design = Design::withTrashed()->find($this->decodePrimaryKey($delivery_note_design_id));
if($design && $design->is_template)
{
$ts = new TemplateService($design);
$pdf = $ts->build([
'invoices' => collect([$this->invoice]),
])->getPdf();
return $pdf;
}
$design_id = $this->invoice->design_id
? $this->invoice->design_id
: $this->decodePrimaryKey($this->invoice->client->getSetting('invoice_design_id'));
@ -70,6 +87,11 @@ class GenerateDeliveryNote
'contact' => $this->contact,
], 'delivery_note'),
'variables' => $html->generateLabelsAndValues(),
'options' => [
'client' => $this->invoice->client,
'entity' => $this->invoice,
'contact' => $this->contact,
],
'process_markdown' => $this->invoice->client->company->markdown_enabled,
];
@ -79,8 +101,6 @@ class GenerateDeliveryNote
->design($template)
->build();
// Storage::makeDirectory($this->invoice->client->invoice_filepath(), 0775);
if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') {
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
} else {
@ -91,11 +111,12 @@ class GenerateDeliveryNote
info($maker->getCompiledHTML());
}
Storage::disk($this->disk)->put($file_path, $pdf);
return $pdf;
// Storage::disk($this->disk)->put($file_path, $pdf);
$maker = null;
$state = null;
return $file_path;
// return $file_path;
}
}

View File

@ -290,6 +290,34 @@ class InvoiceService
return $this;
}
/**
* Reset the reminders if only the
* partial has been paid.
*
* We can _ONLY_ call this _IF_ a partial
* amount has been paid, otherwise we end up wiping
* all reminders regardless
*
* @return self
*/
public function checkReminderStatus(): self
{
if($this->invoice->partial == 0)
$this->invoice->partial_due_date = null;
if($this->invoice->partial == 0 && $this->invoice->balance > 0)
{
$this->invoice->reminder1_sent = null;
$this->invoice->reminder2_sent = null;
$this->invoice->reminder3_sent = null;
$this->setReminder();
}
return $this;
}
public function setReminder($settings = null)
{

View File

@ -49,27 +49,32 @@ class UpdateReminder extends AbstractService
$this->settings->schedule_reminder1 == 'after_invoice_date') {
$reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays($this->settings->num_days_reminder1);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))) {
if ($reminder_date->gt(now())) {
$date_collection->push($reminder_date);
}
}
if (is_null($this->invoice->reminder1_sent) &&
$this->invoice->due_date &&
($this->invoice->partial_due_date || $this->invoice->due_date) &&
$this->settings->schedule_reminder1 == 'before_due_date') {
$reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->subDays($this->settings->num_days_reminder1);
$partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date;
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays($this->settings->num_days_reminder1);
// nlog("1. {$reminder_date->format('Y-m-d')}");
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))) {
if ($reminder_date->gt(now())) {
$date_collection->push($reminder_date);
}
}
if (is_null($this->invoice->reminder1_sent) &&
$this->invoice->due_date &&
($this->invoice->partial_due_date || $this->invoice->due_date) &&
$this->settings->schedule_reminder1 == 'after_due_date') {
$reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->addDays($this->settings->num_days_reminder1);
$partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date;
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays($this->settings->num_days_reminder1);
// nlog("2. {$reminder_date->format('Y-m-d')}");
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))) {
if ($reminder_date->gt(now())) {
$date_collection->push($reminder_date);
}
}
@ -78,27 +83,33 @@ class UpdateReminder extends AbstractService
$this->settings->schedule_reminder2 == 'after_invoice_date') {
$reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays($this->settings->num_days_reminder2);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))) {
if ($reminder_date->gt(now())) {
$date_collection->push($reminder_date);
}
}
if (is_null($this->invoice->reminder2_sent) &&
$this->invoice->due_date &&
($this->invoice->partial_due_date || $this->invoice->due_date) &&
$this->settings->schedule_reminder2 == 'before_due_date') {
$reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->subDays($this->settings->num_days_reminder2);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))) {
$partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date;
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays($this->settings->num_days_reminder2);
// nlog("3. {$reminder_date->format('Y-m-d')}");
if ($reminder_date->gt(now())) {
$date_collection->push($reminder_date);
}
}
if (is_null($this->invoice->reminder2_sent) &&
$this->invoice->due_date &&
($this->invoice->partial_due_date || $this->invoice->due_date) &&
$this->settings->schedule_reminder2 == 'after_due_date') {
$reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->addDays($this->settings->num_days_reminder2);
$partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date;
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays($this->settings->num_days_reminder2);
// nlog("4. {$reminder_date->format('Y-m-d')}");
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))) {
if ($reminder_date->gt(now())) {
$date_collection->push($reminder_date);
}
}
@ -107,27 +118,33 @@ class UpdateReminder extends AbstractService
$this->settings->schedule_reminder3 == 'after_invoice_date') {
$reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays($this->settings->num_days_reminder3);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))) {
if ($reminder_date->gt(now())) {
$date_collection->push($reminder_date);
}
}
if (is_null($this->invoice->reminder3_sent) &&
$this->invoice->due_date &&
($this->invoice->partial_due_date || $this->invoice->due_date) &&
$this->settings->schedule_reminder3 == 'before_due_date') {
$reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->subDays($this->settings->num_days_reminder3);
$partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date;
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays($this->settings->num_days_reminder3);
// nlog("5. {$reminder_date->format('Y-m-d')}");
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))) {
if ($reminder_date->gt(now())) {
$date_collection->push($reminder_date);
}
}
if (is_null($this->invoice->reminder3_sent) &&
$this->invoice->due_date &&
($this->invoice->partial_due_date || $this->invoice->due_date) &&
$this->settings->schedule_reminder3 == 'after_due_date') {
$reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->addDays($this->settings->num_days_reminder3);
$partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date;
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays($this->settings->num_days_reminder3);
// nlog("6. {$reminder_date->format('Y-m-d')}");
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))) {
if ($reminder_date->gt(now())) {
$date_collection->push($reminder_date);
}
}
@ -140,7 +157,7 @@ class UpdateReminder extends AbstractService
$reminder_date = $this->addTimeInterval($this->invoice->last_sent_date, (int) $this->settings->endless_reminder_frequency_id);
if ($reminder_date) {
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))) {
if ($reminder_date->gt(now())) {
$date_collection->push($reminder_date);
}
}

View File

@ -23,11 +23,6 @@ use stdClass;
class RefundPayment
{
public $payment;
public $refund_data;
private $credit_note;
private float $total_refund = 0;
@ -41,12 +36,8 @@ class RefundPayment
private string $refund_failed_message = '';
public function __construct($payment, $refund_data)
public function __construct(public Payment $payment, public array $refund_data)
{
$this->payment = $payment;
$this->refund_data = $refund_data;
$this->gateway_refund_status = false;
$this->activity_repository = new ActivityRepository();
@ -137,6 +128,8 @@ class RefundPayment
$this->payment->refunded += $net_refund;
}
$this->payment->setRefundMeta($this->refund_data);
return $this;
}

View File

@ -55,6 +55,8 @@ class UpdateInvoicePayment
$invoice->restore();
}
// $has_partial = $invoice->hasPartial();
if ($invoice->id == $this->payment_hash->fee_invoice_id) {
$paid_amount = $paid_invoice->amount + $this->payment_hash->fee_total;
} else {
@ -63,6 +65,8 @@ class UpdateInvoicePayment
$client->service()->updatePaidToDate($paid_amount); //always use the payment->amount
$has_partial = $invoice->hasPartial();
/* Need to determine here is we have an OVER payment - if YES only apply the max invoice amount */
if ($paid_amount > $invoice->partial && $paid_amount > $invoice->balance) {
$paid_amount = $invoice->balance;
@ -76,12 +80,16 @@ class UpdateInvoicePayment
$invoice->paid_to_date += $paid_amount;
$invoice->save();
$invoice = $invoice->service()
->clearPartial()
->updateStatus()
// ->deletePdf()
->workFlow()
->save();
$invoice_service = $invoice->service()
->clearPartial()
->updateStatus()
->workFlow();
if ($has_partial) {
$invoice_service->checkReminderStatus();
}
$invoice = $invoice_service->save();
if ($invoice->is_proforma) {
//keep proforma's hidden

View File

@ -11,13 +11,14 @@
namespace App\Services\Pdf;
use App\Models\Credit;
use App\Models\Quote;
use App\Utils\Helpers;
use App\Utils\Traits\MakesDates;
use DOMDocument;
use Illuminate\Support\Carbon;
use App\Models\Quote;
use App\Models\Credit;
use App\Utils\Helpers;
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use App\Utils\Traits\MakesDates;
use App\Services\Template\TemplateService;
use League\CommonMark\CommonMarkConverter;
class PdfBuilder
@ -67,6 +68,7 @@ class PdfBuilder
->buildSections()
->getEmptyElements()
->updateElementProperties()
->parseTwigElements()
->updateVariables();
return $this;
@ -104,6 +106,40 @@ class PdfBuilder
return $this;
}
private function parseTwigElements()
{
$replacements = [];
$contents = $this->document->getElementsByTagName('ninja');
$template_service = new TemplateService();
$data = $template_service->processData($this->service->options)->getData();
$twig = $template_service->twig;
foreach ($contents as $content) {
$template = $content->ownerDocument->saveHTML($content);
$template = $twig->createTemplate(html_entity_decode($template));
$template = $template->render($data);
$f = $this->document->createDocumentFragment();
$f->appendXML($template);
$replacements[] = $f;
}
foreach($contents as $key => $content) {
$content->parentNode->replaceChild($replacements[$key], $content);
}
$contents = null;
return $this;
}
public function setDocument($document): self
{
$this->document = $document;
@ -1091,7 +1127,8 @@ class PdfBuilder
} elseif (Str::startsWith($variable, '$custom_surcharge')) {
$_variable = ltrim($variable, '$'); // $custom_surcharge1 -> custom_surcharge1
$visible = intval($this->service->config->entity->{$_variable}) != 0;
// $visible = intval($this->service->config->entity->{$_variable}) != 0;
$visible = intval(str_replace(['0','.'], '', $this->service->config->entity->{$_variable})) != 0;
$elements[1]['elements'][] = ['element' => 'div', 'elements' => [
['element' => 'span', 'content' => $variable . '_label', 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1) . '-label']],
@ -1318,7 +1355,6 @@ class PdfBuilder
{
$elements = [];
foreach ($variables as $variable) {
$_variable = explode('.', $variable)[1];
$_customs = ['custom1', 'custom2', 'custom3', 'custom4'];

View File

@ -195,24 +195,25 @@ class PdfMock
{
return ['values' =>
[
'$client.shipping_postal_code' => '46420',
'$client.billing_postal_code' => '11243',
'$company.city_state_postal' => 'Beveley Hills, CA, 90210',
'$company.postal_city_state' => 'CA',
'$company.postal_city' => '90210, CA',
'$product.gross_line_total' => '100',
'$client.classification' => 'Individual',
'$company.classification' => 'Business',
'$client.postal_city_state' => '11243 Aufderharchester, North Carolina',
'$client.postal_city' => '11243 Aufderharchester, North Carolina',
'$client.shipping_address1' => '453',
'$client.shipping_address2' => '66327 Waters Trail',
'$client.city_state_postal' => 'Aufderharchester, North Carolina 11243',
'$client.shipping_address' => '453<br/>66327 Waters Trail<br/>Aufderharchester, North Carolina 11243<br/>Afghanistan<br/>',
'$client.shipping_address' => '453<br/>66327 Waters Trail<br/>Aufderharchester, North Carolina 11243<br/>United States<br/>',
'$client.billing_address2' => '63993 Aiyana View',
'$client.billing_address1' => '8447',
'$client.shipping_country' => 'USA',
'$invoiceninja.whitelabel' => 'https://invoicing.co/images/new_logo.png',
'$client.billing_address' => '8447<br/>63993 Aiyana View<br/>Aufderharchester, North Carolina 11243<br/>Afghanistan<br/>',
'$client.billing_address' => '8447<br/>63993 Aiyana View<br/>Aufderharchester, North Carolina 11243<br/>United States<br/>',
'$client.billing_country' => 'USA',
'$task.gross_line_total' => '100',
'$contact.portal_button' => '<a class="button" href="http://ninja.test:8000/client/key_login/zJJEjlUtXPiNnnnyO2tcYia64PSwauidy61eDnMU?client_hash=nzikYQITs1kyUK61GScTNW67JwhTRkOBVdvsHzIv">View client portal</a>',
@ -272,8 +273,8 @@ class PdfMock
'$product.quantity' => '',
'$total_tax_labels' => '',
'$total_tax_values' => '',
'$invoice.discount' => '$0.00',
'$invoice.subtotal' => '$0.00',
'$invoice.discount' => '$5.00',
'$invoice.subtotal' => '$100.00',
'$company.address2' => $this->settings->address2,
'$partial_due_date' => '&nbsp;',
'$invoice.due_date' => '2023-10-24',
@ -310,7 +311,7 @@ class PdfMock
'$created_by_user' => 'Derrick Monahan DDS Erna Wunsch',
'$client.currency' => 'USD',
'$company.country' => 'United States',
'$company.address' => 'United States<br/>',
'$company.address' => 'Christiansen Garden<br/>70218 Lori Station Suite 529<br/>New Loy, Delaware 29359<br/>United States<br/>Phone: 1-240-886-2233<br/>Email: immanuel53@example.net<br/>',
'$tech_hero_image' => 'http://ninja.test:8000/images/pdf-designs/tech-hero-image.jpg',
'$task.tax_name1' => '',
'$task.tax_name2' => '',
@ -329,14 +330,14 @@ class PdfMock
'$invoice.number' => '0029',
'$quote.quote_no' => '0029',
'$quote.datetime' => '25/Feb/2023 1:10 am',
'$client_address' => '8447<br/>63993 Aiyana View<br/>Aufderharchester, North Carolina 11243<br/>Afghanistan<br/>',
'$client.address' => '8447<br/>63993 Aiyana View<br/>Aufderharchester, North Carolina 11243<br/>Afghanistan<br/>',
'$client_address' => '8447<br/>63993 Aiyana View<br/>Aufderharchester, North Carolina 11243<br/>United States<br/>',
'$client.address' => '8447<br/>63993 Aiyana View<br/>Aufderharchester, North Carolina 11243<br/>United States<br/>',
'$payment_button' => '<a class="button" href="http://ninja.test:8000/client/pay/UAUY8vIPuno72igmXbbpldwo5BDDKIqs">Pay Now</a>',
'$payment_qrcode' => '<svg class=\'pqrcode\' viewBox=\'0 0 200 200\' width=\'200\' height=\'200\' x=\'0\' y=\'0\' xmlns=\'http://www.w3.org/2000/svg\'>
<rect x=\'0\' y=\'0\' width=\'100%\'\' height=\'100%\' /><?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="200" height="200" viewBox="0 0 200 200"><rect x="0" y="0" width="200" height="200" fill="#fefefe"/><g transform="scale(4.878)"><g transform="translate(4,4)"><path fill-rule="evenodd" d="M9 0L9 1L8 1L8 2L9 2L9 3L8 3L8 4L10 4L10 7L11 7L11 4L12 4L12 5L13 5L13 4L12 4L12 2L14 2L14 7L15 7L15 6L16 6L16 8L15 8L15 10L14 10L14 11L16 11L16 12L14 12L14 13L15 13L15 14L14 14L14 15L15 15L15 14L17 14L17 15L16 15L16 16L14 16L14 17L15 17L15 18L14 18L14 19L13 19L13 18L11 18L11 15L8 15L8 12L9 12L9 13L10 13L10 14L11 14L11 13L12 13L12 14L13 14L13 13L12 13L12 11L13 11L13 10L11 10L11 11L10 11L10 9L11 9L11 8L6 8L6 9L5 9L5 8L0 8L0 10L1 10L1 12L2 12L2 11L3 11L3 10L4 10L4 11L5 11L5 12L3 12L3 13L7 13L7 14L6 14L6 15L5 15L5 14L1 14L1 15L0 15L0 19L1 19L1 20L0 20L0 25L1 25L1 20L2 20L2 19L3 19L3 20L4 20L4 21L5 21L5 20L6 20L6 21L8 21L8 23L7 23L7 22L5 22L5 24L4 24L4 25L8 25L8 27L10 27L10 28L11 28L11 29L9 29L9 28L8 28L8 33L9 33L9 30L11 30L11 29L12 29L12 32L13 32L13 33L14 33L14 32L15 32L15 33L17 33L17 32L19 32L19 31L18 31L18 30L16 30L16 28L17 28L17 29L18 29L18 28L19 28L19 27L18 27L18 26L17 26L17 27L16 27L16 26L15 26L15 25L16 25L16 24L18 24L18 25L19 25L19 23L18 23L18 22L19 22L19 20L17 20L17 19L20 19L20 25L21 25L21 26L22 26L22 28L21 28L21 27L20 27L20 33L21 33L21 30L24 30L24 32L25 32L25 33L27 33L27 32L29 32L29 33L32 33L32 32L33 32L33 31L31 31L31 32L29 32L29 30L32 30L32 29L33 29L33 27L32 27L32 26L31 26L31 25L32 25L32 24L31 24L31 25L30 25L30 23L29 23L29 21L30 21L30 22L31 22L31 21L32 21L32 22L33 22L33 21L32 21L32 20L33 20L33 18L32 18L32 20L31 20L31 21L30 21L30 19L29 19L29 18L28 18L28 17L25 17L25 16L28 16L28 15L30 15L30 14L31 14L31 17L30 17L30 18L31 18L31 17L32 17L32 16L33 16L33 15L32 15L32 14L31 14L31 13L32 13L32 12L33 12L33 11L32 11L32 10L31 10L31 9L32 9L32 8L31 8L31 9L30 9L30 8L29 8L29 10L28 10L28 11L30 11L30 14L29 14L29 12L27 12L27 11L26 11L26 10L25 10L25 9L26 9L26 8L25 8L25 9L23 9L23 8L24 8L24 7L25 7L25 5L23 5L23 3L24 3L24 4L25 4L25 3L24 3L24 2L25 2L25 0L24 0L24 1L23 1L23 0L21 0L21 1L20 1L20 4L21 4L21 5L22 5L22 7L23 7L23 8L22 8L22 9L18 9L18 8L19 8L19 6L20 6L20 8L21 8L21 6L20 6L20 5L19 5L19 6L18 6L18 5L17 5L17 2L18 2L18 1L19 1L19 0L18 0L18 1L17 1L17 0L16 0L16 1L17 1L17 2L16 2L16 3L15 3L15 2L14 2L14 1L15 1L15 0L14 0L14 1L11 1L11 2L10 2L10 0ZM21 1L21 2L22 2L22 3L23 3L23 2L22 2L22 1ZM10 3L10 4L11 4L11 3ZM15 4L15 5L16 5L16 4ZM8 5L8 7L9 7L9 5ZM12 6L12 9L14 9L14 8L13 8L13 6ZM17 6L17 7L18 7L18 6ZM23 6L23 7L24 7L24 6ZM16 8L16 9L17 9L17 10L16 10L16 11L17 11L17 10L18 10L18 11L20 11L20 10L18 10L18 9L17 9L17 8ZM27 8L27 9L28 9L28 8ZM1 9L1 10L2 10L2 9ZM4 9L4 10L5 10L5 11L6 11L6 12L7 12L7 11L9 11L9 10L8 10L8 9L6 9L6 10L5 10L5 9ZM22 9L22 10L21 10L21 11L22 11L22 10L23 10L23 11L24 11L24 12L23 12L23 13L22 13L22 14L21 14L21 12L18 12L18 13L17 13L17 12L16 12L16 13L17 13L17 14L21 14L21 15L20 15L20 16L19 16L19 15L17 15L17 16L16 16L16 18L21 18L21 19L22 19L22 18L21 18L21 17L22 17L22 16L23 16L23 19L25 19L25 18L24 18L24 16L23 16L23 13L24 13L24 14L25 14L25 12L26 12L26 15L27 15L27 14L28 14L28 13L27 13L27 12L26 12L26 11L24 11L24 10L23 10L23 9ZM6 10L6 11L7 11L7 10ZM30 10L30 11L31 11L31 10ZM10 12L10 13L11 13L11 12ZM1 15L1 17L2 17L2 18L1 18L1 19L2 19L2 18L3 18L3 19L4 19L4 20L5 20L5 19L6 19L6 20L8 20L8 21L10 21L10 23L8 23L8 24L10 24L10 27L11 27L11 26L14 26L14 25L15 25L15 24L16 24L16 23L17 23L17 22L18 22L18 21L17 21L17 20L16 20L16 19L14 19L14 21L13 21L13 19L12 19L12 21L10 21L10 20L11 20L11 18L10 18L10 17L8 17L8 15L6 15L6 16L7 16L7 17L5 17L5 16L4 16L4 15ZM12 15L12 17L13 17L13 15ZM3 16L3 18L4 18L4 19L5 19L5 17L4 17L4 16ZM17 16L17 17L18 17L18 16ZM20 16L20 17L21 17L21 16ZM6 18L6 19L7 19L7 18ZM8 18L8 20L9 20L9 19L10 19L10 18ZM26 18L26 19L27 19L27 20L26 20L26 21L25 21L25 22L24 22L24 20L22 20L22 22L21 22L21 23L22 23L22 25L23 25L23 28L22 28L22 29L24 29L24 30L25 30L25 32L27 32L27 31L28 31L28 30L27 30L27 31L26 31L26 29L24 29L24 24L23 24L23 23L27 23L27 24L29 24L29 23L27 23L27 20L29 20L29 19L27 19L27 18ZM15 20L15 21L14 21L14 23L12 23L12 25L13 25L13 24L14 24L14 23L16 23L16 22L15 22L15 21L16 21L16 20ZM2 21L2 22L3 22L3 23L4 23L4 22L3 22L3 21ZM12 21L12 22L13 22L13 21ZM22 22L22 23L23 23L23 22ZM6 23L6 24L7 24L7 23ZM10 23L10 24L11 24L11 23ZM2 24L2 25L3 25L3 24ZM25 25L25 28L28 28L28 25ZM26 26L26 27L27 27L27 26ZM29 26L29 27L30 27L30 28L29 28L29 29L32 29L32 27L31 27L31 26ZM12 27L12 28L13 28L13 30L14 30L14 29L15 29L15 28L16 28L16 27L15 27L15 28L14 28L14 27ZM17 27L17 28L18 28L18 27ZM15 30L15 31L16 31L16 30ZM10 31L10 32L11 32L11 31ZM13 31L13 32L14 32L14 31ZM22 32L22 33L23 33L23 32ZM0 0L0 7L7 7L7 0ZM1 1L1 6L6 6L6 1ZM2 2L2 5L5 5L5 2ZM26 0L26 7L33 7L33 0ZM27 1L27 6L32 6L32 1ZM28 2L28 5L31 5L31 2ZM0 26L0 33L7 33L7 26ZM1 27L1 32L6 32L6 27ZM2 28L2 31L5 31L5 28Z" fill="#000000"/></g></g></svg>
</svg>',
'$client.country' => 'Afghanistan',
'$client.country' => 'United States',
'$user.last_name' => 'Erna Wunsch',
'$client.website' => 'http://www.parisian.org/',
'$dir_text_align' => 'left',
@ -344,9 +345,9 @@ class PdfMock
'$task.discount' => '',
'$contact.email' => 'bob@gmail.com',
'$primary_color' => isset($this->settings->primary_color) ? $this->settings->primary_color : '#4e4e4e',
'$credit_amount' => '$0.00',
'$invoice.total' => '$0.00',
'$invoice.taxes' => '$0.00',
'$credit_amount' => '$40.00',
'$invoice.total' => '$330.00',
'$invoice.taxes' => '$10.00',
'$quote.custom1' => 'custom value',
'$quote.custom2' => 'custom value',
'$quote.custom3' => 'custom value',
@ -393,10 +394,10 @@ class PdfMock
'$valid_until' => '',
'$your_entity' => '',
'$shipping' => ctrans('texts.shipping_address'),
'$balance_due' => '$0.00',
'$outstanding' => '$0.00',
'$partial_due' => '$0.00',
'$quote.total' => '$0.00',
'$balance_due' => '$1110.00',
'$outstanding' => '$440.00',
'$partial_due' => '$50.00',
'$quote.total' => '$10.00',
'$payment_due' => '&nbsp;',
'$credit.date' => '25/Feb/2023',
'$invoiceDate' => '25/Feb/2023',
@ -454,8 +455,8 @@ class PdfMock
'$view_url' => 'http://ninja.test:8000/client/invoice/UAUY8vIPuno72igmXbbpldwo5BDDKIqs',
'$font_url' => 'https://fonts.googleapis.com/css2?family=Roboto&display=swap',
'$details' => '',
'$balance' => '$0.00',
'$partial' => '$0.00',
'$balance' => '$40.00',
'$partial' => '$30.00',
'$client1' => 'custom value',
'$client2' => 'custom value',
'$client3' => 'custom value',
@ -469,7 +470,7 @@ class PdfMock
'$website' => 'http://www.parisian.org/',
'$entity' => '',
'$thanks' => 'Thanks!',
'$amount' => '$0.00',
'$amount' => '$30.00',
'$method' => '&nbsp;',
'$number' => '0029',
'$footer' => 'Default invoice footer',
@ -479,8 +480,8 @@ class PdfMock
'_rate1' => '',
'_rate2' => '',
'_rate3' => '',
'$taxes' => '$0.00',
'$total' => '$0.00',
'$taxes' => '$40.00',
'$total' => '$10.00',
'$phone' => '&nbsp;',
'$terms' => 'Default company invoice terms',
'$from' => 'Bob Jones',
@ -494,6 +495,8 @@ class PdfMock
'$show_shipping_address' => $this->settings->show_shipping_address ? 'flex' : 'none',
'$show_shipping_address_block' => $this->settings->show_shipping_address ? 'block' : 'none',
'$show_shipping_address_visibility' => $this->settings->show_shipping_address ? 'visible' : 'hidden',
'$start_date' => '31/01/2023',
'$end_date' => '31/12/2023',
],
'labels' => $this->mockTranslatedLabels(),
];
@ -509,6 +512,8 @@ class PdfMock
'$client.billing_postal_code_label' => ctrans('texts.billing_postal_code'),
'$company.postal_city_state_label' => ctrans('texts.postal_city_state'),
'$company.city_state_postal_label' => ctrans('texts.city_state_postal'),
'$client.classification_label' => ctrans('texts.classification'),
'$company.classification_label' => ctrans('texts.classification'),
'$product.gross_line_total_label' => ctrans('texts.gross_line_total'),
'$client.shipping_address1_label' => ctrans('texts.shipping_address1'),
'$client.postal_city_state_label' => ctrans('texts.postal_city_state'),
@ -807,29 +812,31 @@ class PdfMock
'$tax_label' => ctrans('texts.tax'),
'$dir_label' => '',
'$to_label' => ctrans('texts.to'),
'$start_date_label' => ctrans('texts.start_date'),
'$end_date_label' => ctrans('texts.end_date'),
];
}
private function getVendorStubVariables()
{
return ['values' => [
'$vendor.billing_postal_code' => '06270-5526',
'$company.postal_city_state' => '29359 New Loy, Delaware',
'$company.city_state_postal' => 'New Loy, Delaware 29359',
'$product.gross_line_total' => '',
'$purchase_order.po_number' => 'PO12345',
'$vendor.postal_city_state' => '06270-5526 Jameyhaven, West Virginia',
'$vendor.city_state_postal' => 'Jameyhaven, West Virginia 06270-5526',
'$purchase_order.due_date' => '02-12-2021',
'$vendor.billing_address1' => '589',
'$vendor.billing_postal_code' => '06270-5526',
'$company.postal_city_state' => '29359 New Loy, Delaware',
'$company.city_state_postal' => 'New Loy, Delaware 29359',
'$product.gross_line_total' => '',
'$purchase_order.po_number' => 'PO12345',
'$vendor.postal_city_state' => '06270-5526 Jameyhaven, West Virginia',
'$vendor.city_state_postal' => 'Jameyhaven, West Virginia 06270-5526',
'$purchase_order.due_date' => '02-12-2021',
'$vendor.billing_address1' => '589',
'$vendor.billing_address2' => '761 Odessa Centers Suite 673',
'$invoiceninja.whitelabel' => 'https://invoicing.co/images/new_logo.png',
'$purchase_order.custom1' => 'Custom 1',
'$purchase_order.custom2' => 'Custom 2',
'$purchase_order.custom3' => 'Custom 3',
'$purchase_order.custom4' => 'Custom 4',
'$vendor.billing_address' => '589<br/>761 Odessa Centers Suite 673<br/>New Loy, Delaware 29359<br/>Afghanistan<br/>',
'$vendor.billing_country' => 'Afghanistan',
'$vendor.billing_address' => '589<br/>761 Odessa Centers Suite 673<br/>New Loy, Delaware 29359<br/>United States<br/>',
'$vendor.billing_country' => 'United States',
'$purchase_order.number' => 'Live Preview #790',
'$purchase_order.total' => '$10,256.40',
'$vendor.billing_state' => 'West Virginia',
@ -903,9 +910,9 @@ class PdfMock
'$company.website' => 'http://www.dare.com/vero-consequatur-eveniet-dolorum-exercitationem-alias-repellat.html',
'$gross_subtotal' => '$10,256.40',
'$emailSignature' => '&nbsp;',
'$vendor_address' => '589<br/>761 Odessa Centers Suite 673<br/>New Loy, Delaware 29359<br/>Afghanistan<br/>',
'$vendor.address' => '589<br/>761 Odessa Centers Suite 673<br/>New Loy, Delaware 29359<br/>Afghanistan<br/>',
'$vendor.country' => 'Afghanistan',
'$vendor_address' => '589<br/>761 Odessa Centers Suite 673<br/>New Loy, Delaware 29359<br/>United States<br/>',
'$vendor.address' => '589<br/>761 Odessa Centers Suite 673<br/>New Loy, Delaware 29359<br/>United States<br/>',
'$vendor.country' => 'United States',
'$vendor.custom3' => 'Ea quia tempore.',
'$vendor.custom1' => 'Necessitatibus aut.',
'$vendor.custom4' => 'Nobis aut harum.',
@ -1085,7 +1092,7 @@ class PdfMock
'$custom3' => '&nbsp;',
'$custom4' => '&nbsp;',
'$dueDate' => '&nbsp;',
'$country' => 'Afghanistan',
'$country' => 'United States',
'$vendor3' => 'Ea quia tempore.',
'$contact' => 'Geo Maggio',
'$account' => 'Mrs. Kristina Powlowski',

View File

@ -44,6 +44,10 @@ class PdfService
public array $options;
private float $start_time;
public float $execution_time;
const DELIVERY_NOTE = 'delivery_note';
const STATEMENT = 'statement';
const PURCHASE_ORDER = 'purchase_order';
@ -61,7 +65,9 @@ class PdfService
}
public function boot(): self
{
{
$this->start_time = microtime(true);
$this->init();
return $this;
@ -90,6 +96,8 @@ class PdfService
throw new \Exception($e->getMessage(), $e->getCode());
}
$this->execution_time = microtime(true) - $this->start_time;
return $pdf;
}
@ -104,9 +112,11 @@ class PdfService
$html = $this->builder->getCompiledHTML();
if (config('ninja.log_pdf_html')) {
info($html);
nlog($html);
}
$this->execution_time = microtime(true) - $this->start_time;
return $html;
}

View File

@ -322,8 +322,9 @@ class Design extends BaseDesign
public function entityDetails(): array
{
if ($this->type === 'statement') {
// $s_date = $this->translateDate(now(), $this->client->date_format(), $this->client->locale());
$variables = $this->context['pdf_variables']['statement_details'] ?? [];
$s_date = $this->translateDate($this->options['start_date'], $this->client->date_format(), $this->client->locale()) . " - " . $this->translateDate($this->options['end_date'], $this->client->date_format(), $this->client->locale());
return [

View File

@ -241,12 +241,13 @@ trait DesignHelpers
{
// We want to show headers for statements, no exceptions.
$statements = "
document.querySelectorAll('#statement-invoice-table > thead > tr > th, #statement-payment-table > thead > tr > th, #statement-aging-table > thead > tr > th').forEach(t => {
document.querySelectorAll('#statement-credit-table > thead > tr > th, #statement-invoice-table > thead > tr > th, #statement-payment-table > thead > tr > th, #statement-aging-table > thead > tr > th').forEach(t => {
t.hidden = false;
});
";
$javascript = 'document.addEventListener("DOMContentLoaded",function(){document.querySelectorAll("#product-table > tbody > tr > td, #task-table > tbody > tr > td, #delivery-note-table > tbody > tr > td").forEach(t=>{if(""!==t.innerText){let e=t.getAttribute("data-ref").slice(0,-3);document.querySelector(`th[data-ref="${e}-th"]`).removeAttribute("hidden")}}),document.querySelectorAll("#product-table > tbody > tr > td, #task-table > tbody > tr > td, #delivery-note-table > tbody > tr > td").forEach(t=>{let e=t.getAttribute("data-ref").slice(0,-3);(e=document.querySelector(`th[data-ref="${e}-th"]`)).hasAttribute("hidden")&&""==t.innerText&&t.setAttribute("hidden","true")})},!1);';
// $javascript = 'document.addEventListener("DOMContentLoaded",function(){document.querySelectorAll("#product-table > tbody > tr > td, #task-table > tbody > tr > td, #delivery-note-table > tbody > tr > td").forEach(t=>{if(""!==t.innerText){let e=t.getAttribute("data-ref").slice(0,-3);document.querySelector(`th[data-ref="${e}-th"]`).removeAttribute("hidden")}}),document.querySelectorAll("#product-table > tbody > tr > td, #task-table > tbody > tr > td, #delivery-note-table > tbody > tr > td").forEach(t=>{let e=t.getAttribute("data-ref").slice(0,-3);(e=document.querySelector(`th[data-ref="${e}-th"]`)).hasAttribute("hidden")&&""==t.innerText&&t.setAttribute("hidden","true")})},!1);';
$javascript = 'document.addEventListener("DOMContentLoaded",function(){document.querySelectorAll("#custom-table > tbody > tr >td, #product-table > tbody > tr > td, #task-table > tbody > tr > td, #delivery-note-table > tbody > tr > td").forEach(t=>{if(""!==t.innerText){let e=t.getAttribute("data-ref").slice(0,-3);document.querySelector(`th[data-ref="${e}-th"]`).removeAttribute("hidden")}}),document.querySelectorAll("#custom-table > tbody > tr > td, #product-table > tbody > tr > td, #task-table > tbody > tr > td, #delivery-note-table > tbody > tr > td").forEach(t=>{let e=t.getAttribute("data-ref").slice(0,-3);(e=document.querySelector(`th[data-ref="${e}-th"]`)).hasAttribute("hidden")&&""==t.innerText&&t.setAttribute("hidden","true")})},!1);';
// Previously we've been decoding the HTML on the backend and XML parsing isn't good options because it requires,
// strict & valid HTML to even output/decode. Decoding is now done on the frontend with this piece of Javascript.

View File

@ -12,6 +12,7 @@
namespace App\Services\PdfMaker;
use App\Services\Template\TemplateService;
use League\CommonMark\CommonMarkConverter;
class PdfMaker
@ -74,6 +75,32 @@ class PdfMaker
$this->updateElementProperties($this->data['template']);
}
if(isset($this->options)) {
$replacements = [];
$contents = $this->document->getElementsByTagName('ninja');
$twig = (new TemplateService())->twig;
foreach ($contents as $content) {
$template = $content->ownerDocument->saveHTML($content);
$template = $twig->createTemplate(html_entity_decode($template));
$template = $template->render($this->options);
$f = $this->document->createDocumentFragment();
$f->appendXML($template);
$replacements[] = $f;
}
foreach($contents as $key => $content){
$content->parentNode->replaceChild($replacements[$key], $content);
}
}
if (isset($this->data['variables'])) {
$this->updateVariables($this->data['variables']);
}
@ -84,13 +111,14 @@ class PdfMaker
/**
* Final method to get compiled HTML.
*
* @param bool $final @deprecated // is it? i still see it being called elsewhere
* @param bool $final
* @return mixed
*/
public function getCompiledHTML($final = false)
{
$html = $this->document->saveHTML();
$html = $this->document->saveHTML();
// nlog($html);
return str_replace('%24', '$', $html);
}
}

View File

@ -192,6 +192,10 @@ class StubBuilder
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $this->company->markdown_enabled,
'options' => [
'client' => $this->recipient,
'entity' => $this->entity,
],
];
$maker = new PdfMaker($state);

View File

@ -0,0 +1,182 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Template;
use App\Models\Task;
use App\Models\User;
use App\Models\Quote;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Design;
use App\Models\Vendor;
use App\Models\Company;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Product;
use App\Models\Project;
use App\Libraries\MultiDB;
use App\Models\PurchaseOrder;
use Illuminate\Bus\Queueable;
use App\Utils\Traits\MakesHash;
use App\Models\RecurringInvoice;
use App\Services\Email\AdminEmail;
use App\Services\Email\EmailObject;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Queue\Middleware\WithoutOverlapping;
class TemplateAction implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
public $tries = 1;
/**
* Create a new job instance.
*
* @param array $ids The array of entity IDs
* @param string $template The template id
* @param string $entity The entity class name
* @param int $user_id requesting the template
* @param string $db The database name
* @param bool $send_email Determines whether to send an email
*
* @return void
*/
public function __construct(public array $ids,
private string $template,
private string $entity,
private int $user_id,
private Company $company,
private string $db,
private string $hash,
private bool $send_email = false)
{
}
/**
* Execute the job.
*
*/
public function handle()
{
// nlog("inside template action");
MultiDB::setDb($this->db);
$key = $this->resolveEntityString();
$resource = $this->entity::query();
$template = Design::withTrashed()->find($this->decodePrimaryKey($this->template));
$template_service = new TemplateService($template);
match($this->entity){
Invoice::class => $resource->with('payments', 'client'),
Quote::class => $resource->with('client'),
Task::class => $resource->with('client'),
Credit::class => $resource->with('client'),
RecurringInvoice::class => $resource->with('client'),
Project::class => $resource->with('client'),
Expense::class => $resource->with('client'),
Payment::class => $resource->with('invoices', 'client'),
};
$result = $resource->withTrashed()
->whereIn('id', $this->transformKeys($this->ids))
->where('company_id', $this->company->id)
->get();
if($result->count() <= 1)
$data[$key] = collect($result);
else
$data[$key] = $result;
$ts = $template_service->build($data);
// nlog($ts->getHtml());
if($this->send_email) {
$pdf = $ts->getPdf();
$this->sendEmail($pdf, $template);
}
else {
$pdf = $ts->getPdf();
$filename = "templates/{$this->hash}.pdf";
Storage::disk(config('filesystems.default'))->put($filename, $pdf);
return $pdf;
}
}
private function sendEmail(mixed $pdf, Design $template)
{
$user = $this->user_id ? User::find($this->user_id) : $this->company->owner();
$template_name = " [{$template->name}]";
$email_object = new EmailObject;
$email_object->to = [new Address($user->email, $user->present()->name())];
$email_object->attachments = [['file' => base64_encode($pdf), 'name' => ctrans('texts.template') . ".pdf"]];
$email_object->company_key = $this->company->company_key;
$email_object->company = $this->company;
$email_object->settings = $this->company->settings;
$email_object->logo = $this->company->present()->logo();
$email_object->whitelabel = $this->company->account->isPaid() ? true : false;
$email_object->user_id = $user->id;
$email_object->text_body = ctrans('texts.download_report_description') . $template_name;
$email_object->body = ctrans('texts.download_report_description') . $template_name;
$email_object->subject = ctrans('texts.download_report_description') . $template_name;
(new AdminEmail($email_object, $this->company))->handle();
}
/**
* Context
*
* If I have an array of invoices, what could I possib
*
*
*/
private function resolveEntityString()
{
return match ($this->entity) {
Invoice::class => 'invoices',
Quote::class => 'quotes',
Task::class => 'tasks',
Credit::class => 'credits',
RecurringInvoice::class => 'recurring_invoices',
Project::class => 'projects',
Expense::class => 'expenses',
Payment::class => 'payments',
Product::class => 'products',
PurchaseOrder::class => 'purchase_orders',
Project::class => 'projects',
Client::class => 'clients',
Vendor::class => 'vendors',
};
}
public function middleware()
{
return [new WithoutOverlapping("template-{$this->company->company_key}")];
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,779 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Template;
use App\Utils\Number;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Design;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Project;
use App\Models\Activity;
use App\Utils\HtmlEngine;
use League\Fractal\Manager;
use App\Models\PurchaseOrder;
use App\Utils\VendorHtmlEngine;
use App\Utils\PaymentHtmlEngine;
use App\Utils\Traits\MakesDates;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\Traits\Pdf\PdfMaker;
use Twig\Extra\Intl\IntlExtension;
use App\Transformers\TaskTransformer;
use App\Transformers\QuoteTransformer;
use App\Services\Template\TemplateMock;
use App\Transformers\CreditTransformer;
use App\Transformers\InvoiceTransformer;
use App\Transformers\ProjectTransformer;
use App\Transformers\PurchaseOrderTransformer;
use League\Fractal\Serializer\ArraySerializer;
class TemplateService
{
use MakesDates, PdfMaker;
private \DomDocument $document;
public \Twig\Environment $twig;
private string $compiled_html = '';
private array $data = [];
private array $variables = [];
public ?Company $company;
public function __construct(public ?Design $template = null)
{
$this->template = $template;
$this->init();
}
/**
* Boot Dom Document
*
* @return self
*/
private function init(): self
{
$this->document = new \DOMDocument();
$this->document->validateOnParse = true;
$loader = new \Twig\Loader\FilesystemLoader(storage_path());
$this->twig = new \Twig\Environment($loader,[
'debug' => true,
]);
$string_extension = new \Twig\Extension\StringLoaderExtension();
$this->twig->addExtension($string_extension);
$this->twig->addExtension(new IntlExtension());
$this->twig->addExtension(new \Twig\Extension\DebugExtension());
$function = new \Twig\TwigFunction('img', function ($string, $style = '') {
return '<img src="'.$string.'" style="'.$style.'"></img>';
});
$this->twig->addFunction($function);
$filter = new \Twig\TwigFilter('sum', function (array $array, string $column) {
return array_sum(array_column($array, $column));
});
$this->twig->addFilter($filter);
return $this;
}
/**
* Iterate through all of the
* ninja nodes
*
* @param array $data - the payload to be passed into the template
* @return self
*/
public function build(array $data): self
{
$this->compose()
->processData($data)
->parseNinjaBlocks()
->processVariables($data)
->parseVariables();
return $this;
}
private function processVariables($data): self
{
$this->variables = $this->resolveHtmlEngine($data);
return $this;
}
public function mock(): self
{
$tm = new TemplateMock($this->company);
$tm->init();
$this->data = $tm->engines;
$this->variables = $tm->variables[0];
$this->parseNinjaBlocks()
->parseVariables();
return $this;
}
/**
* Returns the HTML as string
*
* @return string
*/
public function getHtml(): string
{
return $this->compiled_html;
}
public function getPdf(): mixed
{
if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') {
$pdf = (new NinjaPdf())->build($this->compiled_html);
} else {
$pdf = $this->makePdf(null, null, $this->compiled_html);
}
return $pdf;
}
public function getData(): array
{
return $this->data;
}
public function processData($data): self
{
$this->data = $this->preProcessDataBlocks($data);
return $this;
}
/**
* Parses all Ninja tags in the document
*
* @return self
*/
private function parseNinjaBlocks(): self
{
$replacements = [];
$contents = $this->document->getElementsByTagName('ninja');
foreach ($contents as $content) {
$template = $content->ownerDocument->saveHTML($content);
try {
$template = $this->twig->createTemplate(html_entity_decode($template));
}
catch(\Twig\Error\SyntaxError $e) {
nlog($e->getMessage());
throw ($e);
}
catch(\Twig\Error\Error $e) {
nlog("error = " .$e->getMessage());
throw ($e);
}
catch(\Twig\Error\RuntimeError $e) {
nlog("runtime = " .$e->getMessage());
throw ($e);
}
catch(\Twig\Error\LoaderError $e) {
nlog("loader = " . $e->getMessage());
throw ($e);
}
catch(\Twig\Error\SecurityError $e) {
nlog("security = " . $e->getMessage());
throw ($e);
}
$template = $template->render($this->data);
$f = $this->document->createDocumentFragment();
$f->appendXML(html_entity_decode($template));
$replacements[] = $f;
}
foreach($contents as $key => $content) {
$content->parentNode->replaceChild($replacements[$key], $content);
}
$this->save();
return $this;
}
/**
* Parses all variables in the document
*
* @return self
*/
private function parseVariables(): self
{
$html = $this->getHtml();
foreach($this->variables as $key => $variable) {
if(isset($variable['labels']) && isset($variable['values']))
{
$html = strtr($html, $variable['labels']);
$html = strtr($html, $variable['values']);
}
}
@$this->document->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$this->save();
return $this;
}
/**
* Saves the document and updates the compiled string.
*
* @return self
*/
private function save(): self
{
$this->compiled_html = str_replace('%24', '$', $this->document->saveHTML());
return $this;
}
/**
* compose
*
* @return self
*/
private function compose(): self
{
if(!$this->template)
return $this;
$html = '';
$html .= $this->template->design->includes;
$html .= $this->template->design->header;
$html .= $this->template->design->body;
$html .= $this->template->design->footer;
@$this->document->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
return $this;
}
/**
* Inject the template components
* manually
*
* @return self
*/
public function setTemplate(array $partials): self
{
$html = '';
$html .= $partials['design']['includes'];
$html .= $partials['design']['header'];
$html .= $partials['design']['body'];
$html .= $partials['design']['footer'];
@$this->document->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
return $this;
}
/**
* Resolves the labels and values needed to replace the string
* holders in the template.
*
* @return array
*/
private function resolveHtmlEngine(array $data): array
{
return collect($data)->map(function ($value, $key) {
$processed = [];
if(in_array($key, ['tasks','projects','aging']) || !$value->first() )
return $processed;
match ($key) {
'variables' => $processed = $value->first() ?? [],
'invoices' => $processed = (new HtmlEngine($value->first()->invitations()->first()))->generateLabelsAndValues() ?? [],
'quotes' => $processed = (new HtmlEngine($value->first()->invitations()->first()))->generateLabelsAndValues() ?? [],
'credits' => $processed = (new HtmlEngine($value->first()->invitations()->first()))->generateLabelsAndValues() ?? [],
'payments' => $processed = (new PaymentHtmlEngine($value->first(), $value->first()->client->contacts()->first()))->generateLabelsAndValues() ?? [],
'tasks' => $processed = [],
'projects' => $processed = [],
'purchase_orders' => (new VendorHtmlEngine($value->first()->invitations()->first()))->generateLabelsAndValues() ?? [],
'aging' => $processed = [],
default => $processed = [],
};
return $processed;
})->toArray();
}
private function preProcessDataBlocks($data): array
{
return collect($data)->map(function ($value, $key){
$processed = [];
match ($key) {
'invoices' => $processed = $this->processInvoices($value),
'quotes' => $processed = $this->processQuotes($value),
'credits' => $processed = $this->processCredits($value),
'payments' => $processed = $this->processPayments($value),
'tasks' => $processed = $this->processTasks($value),
'projects' => $processed = $this->processProjects($value),
'purchase_orders' => $processed = $this->processPurchaseOrders($value),
'aging' => $processed = $value,
default => $processed = [],
};
return $processed;
})->toArray();
}
public function processInvoices($invoices): array
{
$invoices = collect($invoices)
->map(function ($invoice){
$payments = [];
if($invoice->payments ?? false) {
$payments = $invoice->payments->map(function ($payment) {
return $this->transformPayment($payment);
})->toArray();
}
return [
'amount' => Number::formatMoney($invoice->amount, $invoice->client),
'balance' => Number::formatMoney($invoice->balance, $invoice->client),
'balance_raw' => $invoice->balance,
'number' => $invoice->number ?: '',
'discount' => $invoice->discount,
'po_number' => $invoice->po_number ?: '',
'date' => $this->translateDate($invoice->date, $invoice->client->date_format(), $invoice->client->locale()),
'last_sent_date' => $this->translateDate($invoice->last_sent_date, $invoice->client->date_format(), $invoice->client->locale()),
'next_send_date' => $this->translateDate($invoice->next_send_date, $invoice->client->date_format(), $invoice->client->locale()),
'due_date' => $this->translateDate($invoice->due_date, $invoice->client->date_format(), $invoice->client->locale()),
'terms' => $invoice->terms ?: '',
'public_notes' => $invoice->public_notes ?: '',
'private_notes' => $invoice->private_notes ?: '',
'uses_inclusive_taxes' => (bool) $invoice->uses_inclusive_taxes,
'tax_name1' => $invoice->tax_name1 ?? '',
'tax_rate1' => (float) $invoice->tax_rate1,
'tax_name2' => $invoice->tax_name2 ?? '',
'tax_rate2' => (float) $invoice->tax_rate2,
'tax_name3' => $invoice->tax_name3 ?? '',
'tax_rate3' => (float) $invoice->tax_rate3,
'total_taxes' => Number::formatMoney($invoice->total_taxes, $invoice->client),
'total_taxes_raw' => $invoice->total_taxes,
'is_amount_discount' => (bool) $invoice->is_amount_discount ?? false,
'footer' => $invoice->footer ?? '',
'partial' => $invoice->partial ?? 0,
'partial_due_date' => $this->translateDate($invoice->partial_due_date, $invoice->client->date_format(), $invoice->client->locale()),
'custom_value1' => (string) $invoice->custom_value1 ?: '',
'custom_value2' => (string) $invoice->custom_value2 ?: '',
'custom_value3' => (string) $invoice->custom_value3 ?: '',
'custom_value4' => (string) $invoice->custom_value4 ?: '',
'custom_surcharge1' => (float) $invoice->custom_surcharge1,
'custom_surcharge2' => (float) $invoice->custom_surcharge2,
'custom_surcharge3' => (float) $invoice->custom_surcharge3,
'custom_surcharge4' => (float) $invoice->custom_surcharge4,
'exchange_rate' => (float) $invoice->exchange_rate,
'custom_surcharge_tax1' => (bool) $invoice->custom_surcharge_tax1,
'custom_surcharge_tax2' => (bool) $invoice->custom_surcharge_tax2,
'custom_surcharge_tax3' => (bool) $invoice->custom_surcharge_tax3,
'custom_surcharge_tax4' => (bool) $invoice->custom_surcharge_tax4,
'line_items' => $invoice->line_items ? $this->padLineItems($invoice->line_items, $invoice->client): (array) [],
'reminder1_sent' => $this->translateDate($invoice->reminder1_sent, $invoice->client->date_format(), $invoice->client->locale()),
'reminder2_sent' => $this->translateDate($invoice->reminder2_sent, $invoice->client->date_format(), $invoice->client->locale()),
'reminder3_sent' => $this->translateDate($invoice->reminder3_sent, $invoice->client->date_format(), $invoice->client->locale()),
'reminder_last_sent' => $this->translateDate($invoice->reminder_last_sent, $invoice->client->date_format(), $invoice->client->locale()),
'paid_to_date' => Number::formatMoney($invoice->paid_to_date, $invoice->client),
'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled,
'client' => [
'name' => $invoice->client->present()->name(),
'balance' => $invoice->client->balance,
'payment_balance' => $invoice->client->payment_balance,
'credit_balance' => $invoice->client->credit_balance,
],
'payments' => $payments,
'total_tax_map' => $invoice->calc()->getTotalTaxMap(),
'line_tax_map' => $invoice->calc()->getTaxMap(),
];
});
return $invoices->toArray();
}
public function padLineItems(array $items, Client $client): array
{
return collect($items)->map(function ($item) use ($client){
$item->cost_raw = $item->cost ?? 0;
$item->discount_raw = $item->discount ?? 0;
$item->line_total_raw = $item->line_total ?? 0;
$item->gross_line_total_raw = $item->gross_line_total ?? 0;
$item->tax_amount_raw = $item->tax_amount ?? 0;
$item->product_cost_raw = $item->product_cost ?? 0;
$item->cost = Number::formatMoney($item->cost_raw, $client);
if($item->is_amount_discount)
$item->discount = Number::formatMoney($item->discount_raw, $client);
$item->line_total = Number::formatMoney($item->line_total_raw, $client);
$item->gross_line_total = Number::formatMoney($item->gross_line_total_raw, $client);
$item->tax_amount = Number::formatMoney($item->tax_amount_raw, $client);
$item->product_cost = Number::formatMoney($item->product_cost_raw, $client);
return $item;
})->toArray();
}
private function transformPayment(Payment $payment): array
{
$data = [];
$credits = $payment->credits->map(function ($credit) use ($payment) {
return [
'credit' => $credit->number,
'amount_raw' => $credit->pivot->amount,
'refunded_raw' => $credit->pivot->refunded,
'net_raw' => $credit->pivot->amount - $credit->pivot->refunded,
'amount' => Number::formatMoney($credit->pivot->amount, $payment->client),
'refunded' => Number::formatMoney($credit->pivot->refunded, $payment->client),
'net' => Number::formatMoney($credit->pivot->amount - $credit->pivot->refunded, $payment->client),
'is_credit' => true,
'date' => $this->translateDate($credit->date, $payment->client->date_format(), $payment->client->locale()),
'created_at' => $this->translateDate($credit->pivot->created_at, $payment->client->date_format(), $payment->client->locale()),
'updated_at' => $this->translateDate($credit->pivot->updated_at, $payment->client->date_format(), $payment->client->locale()),
'timestamp' => $credit->pivot->created_at->timestamp,
];
});
$pivot = $payment->invoices->map(function ($invoice) use ($payment) {
return [
'invoice' => $invoice->number,
'amount_raw' => $invoice->pivot->amount,
'refunded_raw' => $invoice->pivot->refunded,
'net_raw' => $invoice->pivot->amount - $invoice->pivot->refunded,
'amount' => Number::formatMoney($invoice->pivot->amount, $payment->client),
'refunded' => Number::formatMoney($invoice->pivot->refunded, $payment->client),
'net' => Number::formatMoney($invoice->pivot->amount - $invoice->pivot->refunded, $payment->client),
'is_credit' => false,
'date' => $this->translateDate($invoice->date, $payment->client->date_format(), $payment->client->locale()),
'created_at' => $this->translateDate($invoice->pivot->created_at, $payment->client->date_format(), $payment->client->locale()),
'updated_at' => $this->translateDate($invoice->pivot->updated_at, $payment->client->date_format(), $payment->client->locale()),
'timestamp' => $invoice->pivot->created_at->timestamp,
];
})->merge($credits)->sortBy('timestamp')->toArray();
return [
'status' => $payment->stringStatus($payment->status_id),
'badge' => $payment->badgeForStatus($payment->status_id),
'amount' => Number::formatMoney($payment->amount, $payment->client),
'applied' => Number::formatMoney($payment->applied, $payment->client),
'balance' => Number::formatMoney(($payment->amount - $payment->refunded - $payment->applied), $payment->client),
'refunded' => Number::formatMoney($payment->refunded, $payment->client),
'amount_raw' => $payment->amount,
'applied_raw' => $payment->applied,
'refunded_raw' => $payment->refunded,
'balance_raw' => ($payment->amount - $payment->refunded - $payment->applied),
'date' => $this->translateDate($payment->date, $payment->client->date_format(), $payment->client->locale()),
'method' => $payment->translatedType(),
'currency' => $payment->currency->code,
'exchange_rate' => $payment->exchange_rate,
'transaction_reference' => $payment->transaction_reference,
'is_manual' => $payment->is_manual,
'number' => $payment->number,
'custom_value1' => $payment->custom_value1 ?? '',
'custom_value2' => $payment->custom_value2 ?? '',
'custom_value3' => $payment->custom_value3 ?? '',
'custom_value4' => $payment->custom_value4 ?? '',
'created_at' => $this->translateDate($payment->created_at, $payment->client->date_format(), $payment->client->locale()),
'updated_at' => $this->translateDate($payment->updated_at, $payment->client->date_format(), $payment->client->locale()),
'client' => [
'name' => $payment->client->present()->name(),
'balance' => $payment->client->balance,
'payment_balance' => $payment->client->payment_balance,
'credit_balance' => $payment->client->credit_balance,
],
'paymentables' => $pivot,
'refund_activity' => $this->getPaymentRefundActivity($payment),
];
nlog($data);
return $data;
}
/**
* [
"id" => 12,
"date" => "2023-10-08",
"invoices" => [
[
"amount" => 1,
"invoice_id" => 23,
"id" => null,
],
],
"q" => "/api/v1/payments/refund",
"email_receipt" => "true",
"gateway_refund" => false,
"send_email" => false,
],
*
* @param Payment $payment
* @return array
*/
private function getPaymentRefundActivity(Payment $payment): array
{
return collect($payment->refund_meta ?? [])
->map(function ($refund) use($payment){
$date = \Carbon\Carbon::parse($refund['date'])->addSeconds($payment->client->timezone_offset());
$date = $this->translateDate($date, $payment->client->date_format(), $payment->client->locale());
$entity = ctrans('texts.invoice');
$map = [];
foreach($refund['invoices'] as $refunded_invoice) {
$invoice = Invoice::withTrashed()->find($refunded_invoice['invoice_id']);
$amount = Number::formatMoney($refunded_invoice['amount'], $payment->client);
$notes = ctrans('texts.status_partially_refunded_amount', ['amount' => $amount]);
array_push($map, "{$date} {$entity} #{$invoice->number} {$notes}\n");
}
return $map;
})->flatten()->toArray();
}
public function processQuotes($quotes): array
{
$it = new QuoteTransformer();
$it->setDefaultIncludes(['client']);
$manager = new Manager();
$manager->parseIncludes(['client']);
$resource = new \League\Fractal\Resource\Collection($quotes, $it, null);
$resources = $manager->createData($resource)->toArray();
foreach($resources['data'] as $key => $resource) {
$resources['data'][$key]['client'] = $resource['client']['data'] ?? [];
$resources['data'][$key]['client']['contacts'] = $resource['client']['data']['contacts']['data'] ?? [];
}
return $resources['data'];
}
/**
* Pushes credits through the appropriate transformer
* and builds any required relationships
*
* @param mixed $credits
* @return array
*/
public function processCredits($credits): array
{
$credits = collect($credits)
->map(function ($credit){
return [
'amount' => Number::formatMoney($credit->amount, $credit->client),
'balance' => Number::formatMoney($credit->balance, $credit->client),
'balance_raw' => $credit->balance,
'number' => $credit->number ?: '',
'discount' => $credit->discount,
'po_number' => $credit->po_number ?: '',
'date' => $this->translateDate($credit->date, $credit->client->date_format(), $credit->client->locale()),
'last_sent_date' => $this->translateDate($credit->last_sent_date, $credit->client->date_format(), $credit->client->locale()),
'next_send_date' => $this->translateDate($credit->next_send_date, $credit->client->date_format(), $credit->client->locale()),
'due_date' => $this->translateDate($credit->due_date, $credit->client->date_format(), $credit->client->locale()),
'terms' => $credit->terms ?: '',
'public_notes' => $credit->public_notes ?: '',
'private_notes' => $credit->private_notes ?: '',
'uses_inclusive_taxes' => (bool) $credit->uses_inclusive_taxes,
'tax_name1' => $credit->tax_name1 ?? '',
'tax_rate1' => (float) $credit->tax_rate1,
'tax_name2' => $credit->tax_name2 ?? '',
'tax_rate2' => (float) $credit->tax_rate2,
'tax_name3' => $credit->tax_name3 ?? '',
'tax_rate3' => (float) $credit->tax_rate3,
'total_taxes' => Number::formatMoney($credit->total_taxes, $credit->client),
'total_taxes_raw' => $credit->total_taxes,
'is_amount_discount' => (bool) $credit->is_amount_discount ?? false,
'footer' => $credit->footer ?? '',
'partial' => $credit->partial ?? 0,
'partial_due_date' => $this->translateDate($credit->partial_due_date, $credit->client->date_format(), $credit->client->locale()),
'custom_value1' => (string) $credit->custom_value1 ?: '',
'custom_value2' => (string) $credit->custom_value2 ?: '',
'custom_value3' => (string) $credit->custom_value3 ?: '',
'custom_value4' => (string) $credit->custom_value4 ?: '',
'custom_surcharge1' => (float) $credit->custom_surcharge1,
'custom_surcharge2' => (float) $credit->custom_surcharge2,
'custom_surcharge3' => (float) $credit->custom_surcharge3,
'custom_surcharge4' => (float) $credit->custom_surcharge4,
'exchange_rate' => (float) $credit->exchange_rate,
'custom_surcharge_tax1' => (bool) $credit->custom_surcharge_tax1,
'custom_surcharge_tax2' => (bool) $credit->custom_surcharge_tax2,
'custom_surcharge_tax3' => (bool) $credit->custom_surcharge_tax3,
'custom_surcharge_tax4' => (bool) $credit->custom_surcharge_tax4,
'line_items' => $credit->line_items ? $this->padLineItems($credit->line_items, $credit->client): (array) [],
'reminder1_sent' => $this->translateDate($credit->reminder1_sent, $credit->client->date_format(), $credit->client->locale()),
'reminder2_sent' => $this->translateDate($credit->reminder2_sent, $credit->client->date_format(), $credit->client->locale()),
'reminder3_sent' => $this->translateDate($credit->reminder3_sent, $credit->client->date_format(), $credit->client->locale()),
'reminder_last_sent' => $this->translateDate($credit->reminder_last_sent, $credit->client->date_format(), $credit->client->locale()),
'paid_to_date' => Number::formatMoney($credit->paid_to_date, $credit->client),
'auto_bill_enabled' => (bool) $credit->auto_bill_enabled,
'client' => [
'name' => $credit->client->present()->name(),
'balance' => $credit->client->balance,
'payment_balance' => $credit->client->payment_balance,
'credit_balance' => $credit->client->credit_balance,
],
'payments' => [],
'total_tax_map' => $credit->calc()->getTotalTaxMap(),
'line_tax_map' => $credit->calc()->getTaxMap(),
];
});
return $credits->toArray();
}
/**
* Pushes payments through the appropriate transformer
*
* @param mixed $payments
* @return array
*/
public function processPayments($payments): array
{
$payments = collect($payments)->map(function ($payment) {
return $this->transformPayment($payment);
})->toArray();
return $payments;
}
public function processTasks($tasks): array
{
$it = new TaskTransformer();
$it->setDefaultIncludes(['client','project','invoice']);
$manager = new Manager();
$resource = new \League\Fractal\Resource\Collection($tasks, $it, null);
$resources = $manager->createData($resource)->toArray();
foreach($resources['data'] as $key => $resource) {
$resources['data'][$key]['client'] = $resource['client']['data'] ?? [];
$resources['data'][$key]['client']['contacts'] = $resource['client']['data']['contacts']['data'] ?? [];
$resources['data'][$key]['project'] = $resource['project']['data'] ?? [];
$resources['data'][$key]['invoice'] = $resource['invoice'] ?? [];
}
return $resources['data'];
}
public function processProjects($projects): array
{
$it = new ProjectTransformer();
$it->setDefaultIncludes(['client','tasks']);
$manager = new Manager();
$manager->setSerializer(new ArraySerializer());
$resource = new \League\Fractal\Resource\Collection($projects, $it, Project::class);
$i = $manager->createData($resource)->toArray();
return $i[Project::class];
}
public function processPurchaseOrders($purchase_orders): array
{
$it = new PurchaseOrderTransformer();
$it->setDefaultIncludes(['vendor','expense']);
$manager = new Manager();
$manager->setSerializer(new ArraySerializer());
$resource = new \League\Fractal\Resource\Collection($purchase_orders, $it, PurchaseOrder::class);
$i = $manager->createData($resource)->toArray();
return $i[PurchaseOrder::class];
}
public function setCompany(Company $company): self
{
$this->company = $company;
return $this;
}
public function getCompany(): Company
{
return $this->company;
}
public function overrideVariables($variables): self
{
$this->variables = $variables;
return $this;
}
}

View File

@ -47,12 +47,15 @@ class DesignTransformer extends EntityTransformer
'name' => (string) $design->name,
'is_custom' => (bool) $design->is_custom,
'is_active' => (bool) $design->is_active,
'is_template' => (bool) $design->is_template,
'design' => $design->design,
'updated_at' => (int) $design->updated_at,
'archived_at' => (int) $design->deleted_at,
'created_at' => (int) $design->created_at,
'is_deleted' => (bool) $design->is_deleted,
'is_free' => ($design->id <= 4) ? true : false,
'is_template' => (bool) $design->is_template,
'entities' => (string) $design->entities ?: '',
];
}
}

View File

@ -14,6 +14,7 @@ namespace App\Transformers;
use App\Models\Activity;
use App\Models\Backup;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Document;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
@ -63,6 +64,13 @@ class InvoiceTransformer extends EntityTransformer
return $this->includeCollection($invoice->payments, $transformer, Payment::class);
}
public function includeCredits(Invoice $invoice)
{
$transformer = new CreditTransformer($this->serializer);
return $this->includeCollection($invoice->credits, $transformer, Credit::class);
}
/*
public function includeExpenses(Invoice $invoice)
{

View File

@ -12,10 +12,12 @@
namespace App\Transformers;
use App\Models\Client;
use App\Models\Document;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Document;
use App\Models\Paymentable;
use App\Models\PaymentType;
use App\Utils\Traits\MakesHash;
class PaymentTransformer extends EntityTransformer
@ -32,6 +34,8 @@ class PaymentTransformer extends EntityTransformer
protected array $availableIncludes = [
'client',
'invoices',
'type',
'credits',
];
public function __construct($serializer = null)
@ -48,6 +52,13 @@ class PaymentTransformer extends EntityTransformer
return $this->includeCollection($payment->invoices, $transformer, Invoice::class);
}
public function includeCredits(Payment $payment)
{
$transformer = new CreditTransformer($this->serializer);
return $this->includeCollection($payment->credits, $transformer, Credit::class);
}
public function includeClient(Payment $payment)
{
$transformer = new ClientTransformer($this->serializer);
@ -69,6 +80,11 @@ class PaymentTransformer extends EntityTransformer
return $this->includeCollection($payment->documents, $transformer, Document::class);
}
public function includeType(Payment $payment)
{
return $this->includeItem($payment, new PaymentTypeTransformer, PaymentType::class);
}
public function transform(Payment $payment)
{
return [

View File

@ -0,0 +1,25 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Transformers;
use App\Models\Payment;
class PaymentTypeTransformer extends EntityTransformer
{
public function transform(Payment $payment)
{
return [
'name' => $payment->translatedType()
];
}
}

View File

@ -55,7 +55,7 @@ class TaskTransformer extends EntityTransformer
{
$transformer = new InvoiceTransformer($this->serializer);
if (!$task->user) {
if (!$task->invoice) {
return null;
}

View File

@ -460,7 +460,7 @@ class HtmlEngine
$data['$client.postal_city'] = &$data['$postal_city'];
$data['$client.country'] = &$data['$country'];
$data['$client.email'] = &$data['$email'];
$data['$client.classification'] = ['value' => isset($this->client->classification) ? ctrans("texts.{$this->client->classification}") : ' ', 'label' => ctrans('texts.classification')];
$data['$client.billing_address'] = &$data['$client_address'];
$data['$client.billing_address1'] = &$data['$client.address1'];
$data['$client.billing_address2'] = &$data['$client.address2'];
@ -516,6 +516,7 @@ class HtmlEngine
$data['$company.postal_city_state'] = ['value' => $this->company->present()->cityStateZip($this->settings->city, $this->settings->state, $this->settings->postal_code, true) ?: ' ', 'label' => ctrans('texts.postal_city_state')];
$data['$company.postal_city'] = ['value' => $this->company->present()->cityStateZip($this->settings->city, null, $this->settings->postal_code, true) ?: ' ', 'label' => ctrans('texts.postal_city')];
$data['$company.name'] = ['value' => $this->settings->name ?: ctrans('texts.untitled_account'), 'label' => ctrans('texts.company_name')];
$data['$company.classification'] = ['value' => ($this->settings->classification ?? false) ? ctrans("texts.{$this->settings->classification}") : ' ', 'label' => ctrans('texts.classification')];
$data['$account'] = &$data['$company.name'];
$data['$company.address1'] = ['value' => $this->settings->address1 ?: ' ', 'label' => ctrans('texts.address1')];
@ -745,6 +746,20 @@ class HtmlEngine
return $data;
}
public function makeValuesNoPrefix() :array
{
$data = [];
$values = $this->buildEntityDataArray();
foreach ($values as $key => $value) {
$data[str_replace(["$","."],["_","_"],$key)] = $value['value'];
}
return $data;
}
public function generateLabelsAndValues()
{
$data = [];

View File

@ -64,7 +64,15 @@ class PaymentHtmlEngine
$data['$amount'] = &$data['$payment.amount'];
$data['$payment.date'] = ['value' => $this->translateDate($this->payment->date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.payment_date')];
$data['$transaction_reference'] = ['value' => $this->payment->transaction_reference, 'label' => ctrans('texts.transaction_reference')];
// $data['$public_notes'] = ['value' => $this->payment->public_notes, 'label' => ctrans('texts.notes')];
$data['$font_size'] = ['value' => $this->settings->font_size . 'px !important;', '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['$secondary_font_name'] = ['value' => Helpers::resolveFont($this->settings->secondary_font)['name'], 'label' => ''];
$data['$secondary_font_url'] = ['value' => Helpers::resolveFont($this->settings->secondary_font)['url'], 'label' => ''];
$data['$invoiceninja.whitelabel'] = ['value' => 'https://invoicing.co/images/new_logo.png', 'label' => ''];
$data['$primary_color'] = ['value' => $this->settings->primary_color, 'label' => ''];
$data['$secondary_color'] = ['value' => $this->settings->secondary_color, 'label' => ''];
$data['$payment1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'payment1', $this->payment->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment1')];
$data['$payment2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'payment2', $this->payment->custom_value2, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment2')];

View File

@ -214,6 +214,8 @@ class Phantom
'options' => [
'all_pages_header' => $entity_obj->client->getSetting('all_pages_header'),
'all_pages_footer' => $entity_obj->client->getSetting('all_pages_footer'),
'client' => $entity_obj->client,
'entity' => $entity_obj,
],
'process_markdown' => $entity_obj->client->company->markdown_enabled,
];
@ -230,4 +232,5 @@ class Phantom
return view('pdf.html', $data);
}
}

View File

@ -28,6 +28,15 @@ use App\Jobs\Company\CompanyTaxRate;
*/
trait CompanySettingsSaver
{
private array $string_ids = [
'payment_refund_design_id',
'payment_receipt_design_id',
'delivery_note_design_id',
'statement_design_id',
'besr_id',
'gmail_sending_user_id',
];
/**
* Saves a setting object.
*
@ -131,7 +140,8 @@ trait CompanySettingsSaver
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter') {
$value = 'integer';
if ($key == 'besr_id') {
if(in_array($key, $this->string_ids)) {
// if ($key == 'besr_id') {
$value = 'string';
}
@ -199,13 +209,17 @@ trait CompanySettingsSaver
if (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter') {
$value = 'integer';
if ($key == 'gmail_sending_user_id') {
$value = 'string';
if(in_array($key, $this->string_ids)) {
$value ='string';
}
if ($key == 'besr_id') {
$value = 'string';
}
// if ($key == 'gmail_sending_user_id') {
// $value = 'string';
// }
// if ($key == 'besr_id') {
// $value = 'string';
// }
if (! property_exists($settings, $key)) {
continue;

View File

@ -34,9 +34,11 @@ trait MakesReminders
case 'after_invoice_date':
return Carbon::parse($this->date)->addDays($num_days_reminder)->startOfDay()->addSeconds($offset)->isSameDay(Carbon::now());
case 'before_due_date':
return Carbon::parse($this->due_date)->subDays($num_days_reminder)->startOfDay()->addSeconds($offset)->isSameDay(Carbon::now());
$partial_or_due_date = ($this->partial > 0 && isset($this->partial_due_date)) ? $this->partial_due_date : $this->due_date;
return Carbon::parse($partial_or_due_date)->subDays($num_days_reminder)->startOfDay()->addSeconds($offset)->isSameDay(Carbon::now());
case 'after_due_date':
return Carbon::parse($this->due_date)->addDays($num_days_reminder)->startOfDay()->addSeconds($offset)->isSameDay(Carbon::now());
$partial_or_due_date = ($this->partial > 0 && isset($this->partial_due_date)) ? $this->partial_due_date : $this->due_date;
return Carbon::parse($partial_or_due_date)->addDays($num_days_reminder)->startOfDay()->addSeconds($offset)->isSameDay(Carbon::now());
default:
return null;
}

View File

@ -18,6 +18,15 @@ use App\DataMapper\CompanySettings;
*/
trait SettingsSaver
{
private array $string_ids = [
'payment_refund_design_id',
'payment_receipt_design_id',
'delivery_note_design_id',
'statement_design_id',
'besr_id',
'gmail_sending_user_id',
];
/**
* Used for custom validation of inbound
* settings request.
@ -54,7 +63,8 @@ trait SettingsSaver
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter' || ($key == 'payment_terms' && property_exists($settings, $key) && strlen($settings->{$key}) >= 1) || ($key == 'valid_until' && property_exists($settings, $key) && strlen($settings->{$key}) >= 1)) {
$value = 'integer';
if ($key == 'gmail_sending_user_id' || $key == 'besr_id') {
if(in_array($key, $this->string_ids)) {
// if ($key == 'gmail_sending_user_id' || $key == 'besr_id') {
$value = 'string';
}

View File

@ -31,7 +31,7 @@
],
"type": "project",
"require": {
"php": "^8.1",
"php": "^8.1|^8.2",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
@ -76,6 +76,7 @@
"omnipay/paypal": "^3.0",
"payfast/payfast-php-sdk": "^1.1",
"pragmarx/google2fa": "^8.0",
"predis/predis": "^2",
"psr/http-message": "^1.0",
"pusher/pusher-php-server": "^7.2",
"razorpay/razorpay": "2.*",
@ -93,14 +94,15 @@
"symfony/mailgun-mailer": "^6.1",
"symfony/postmark-mailer": "^6.1",
"turbo124/beacon": "^1.5",
"predis/predis": "^2",
"twig/intl-extra": "^3.7",
"twig/twig": "^3",
"twilio/sdk": "^6.40",
"webpatser/laravel-countries": "dev-master#75992ad",
"wepay/php-sdk": "^0.3",
"wildbit/postmark-php": "^4.0"
},
"require-dev": {
"php": "^8.1",
"php": "^8.1|^8.2",
"barryvdh/laravel-debugbar": "^3.6",
"barryvdh/laravel-ide-helper": "^2.13",
"brianium/paratest": "^7",

305
composer.lock generated
View File

@ -4,7 +4,11 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
<<<<<<< HEAD
"content-hash": "f0ad0b9b101d54a8530ab494539e9590",
=======
"content-hash": "0e0f7606a875b132577ee735309b1247",
>>>>>>> support_for_custom_statement_designs
"packages": [
{
"name": "afosto/yaac",
@ -485,6 +489,7 @@
},
{
"name": "aws/aws-sdk-php",
<<<<<<< HEAD
"version": "3.283.0",
"source": {
"type": "git",
@ -495,6 +500,18 @@
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5084c03431ecda0003e35d7fc7a12eeca4242685",
"reference": "5084c03431ecda0003e35d7fc7a12eeca4242685",
=======
"version": "3.282.0",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "79a3ed5bb573f592823f8b1cffe0dbac3132e6b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/79a3ed5bb573f592823f8b1cffe0dbac3132e6b4",
"reference": "79a3ed5bb573f592823f8b1cffe0dbac3132e6b4",
>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@ -574,9 +591,15 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
<<<<<<< HEAD
"source": "https://github.com/aws/aws-sdk-php/tree/3.283.0"
},
"time": "2023-10-04T18:08:32+00:00"
=======
"source": "https://github.com/aws/aws-sdk-php/tree/3.282.0"
},
"time": "2023-09-28T18:09:20+00:00"
>>>>>>> support_for_custom_statement_designs
},
{
"name": "bacon/bacon-qr-code",
@ -4287,6 +4310,7 @@
},
{
"name": "laravel/framework",
<<<<<<< HEAD
"version": "v10.26.2",
"source": {
"type": "git",
@ -4297,6 +4321,18 @@
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/6e5440f7c518f26b4495e5d7e4796ec239e26df9",
"reference": "6e5440f7c518f26b4495e5d7e4796ec239e26df9",
=======
"version": "v10.25.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "6014dd456b414b305fb0b408404efdcec18e64bc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/6014dd456b414b305fb0b408404efdcec18e64bc",
"reference": "6014dd456b414b305fb0b408404efdcec18e64bc",
>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@ -4483,6 +4519,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
<<<<<<< HEAD
"time": "2023-10-03T14:24:20+00:00"
},
{
@ -4497,6 +4534,22 @@
"type": "zip",
"url": "https://api.github.com/repos/laravel/prompts/zipball/cce65a90e64712909ea1adc033e1d88de8455ffd",
"reference": "cce65a90e64712909ea1adc033e1d88de8455ffd",
=======
"time": "2023-09-28T14:08:59+00:00"
},
{
"name": "laravel/prompts",
"version": "v0.1.10",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
"reference": "37ed55f6950d921a87d5beeab16d03f8de26b060"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/prompts/zipball/37ed55f6950d921a87d5beeab16d03f8de26b060",
"reference": "37ed55f6950d921a87d5beeab16d03f8de26b060",
>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@ -4538,9 +4591,15 @@
],
"support": {
"issues": "https://github.com/laravel/prompts/issues",
<<<<<<< HEAD
"source": "https://github.com/laravel/prompts/tree/v0.1.11"
},
"time": "2023-10-03T01:07:35+00:00"
=======
"source": "https://github.com/laravel/prompts/tree/v0.1.10"
},
"time": "2023-09-29T07:26:07+00:00"
>>>>>>> support_for_custom_statement_designs
},
{
"name": "laravel/serializable-closure",
@ -9560,6 +9619,7 @@
},
{
"name": "sentry/sentry-laravel",
<<<<<<< HEAD
"version": "3.8.1",
"source": {
"type": "git",
@ -9570,6 +9630,18 @@
"type": "zip",
"url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/b6142a80fa9360a10b786d2da032339602d0e362",
"reference": "b6142a80fa9360a10b786d2da032339602d0e362",
=======
"version": "3.8.0",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-laravel.git",
"reference": "c7e7611553f9f90af10ed98dde1a680220f02e4d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/c7e7611553f9f90af10ed98dde1a680220f02e4d",
"reference": "c7e7611553f9f90af10ed98dde1a680220f02e4d",
>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@ -9636,7 +9708,11 @@
],
"support": {
"issues": "https://github.com/getsentry/sentry-laravel/issues",
<<<<<<< HEAD
"source": "https://github.com/getsentry/sentry-laravel/tree/3.8.1"
=======
"source": "https://github.com/getsentry/sentry-laravel/tree/3.8.0"
>>>>>>> support_for_custom_statement_designs
},
"funding": [
{
@ -9648,7 +9724,11 @@
"type": "custom"
}
],
<<<<<<< HEAD
"time": "2023-10-04T10:21:16+00:00"
=======
"time": "2023-09-05T11:02:34+00:00"
>>>>>>> support_for_custom_statement_designs
},
{
"name": "setasign/fpdf",
@ -13872,6 +13952,144 @@
"source": "https://github.com/turbo124/beacon/tree/v1.5.2"
},
"time": "2023-10-01T07:13:02+00:00"
<<<<<<< HEAD
=======
},
{
"name": "twig/intl-extra",
"version": "v3.7.1",
"source": {
"type": "git",
"url": "https://github.com/twigphp/intl-extra.git",
"reference": "4f4fe572f635534649cc069e1dafe4a8ad63774d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/4f4fe572f635534649cc069e1dafe4a8ad63774d",
"reference": "4f4fe572f635534649cc069e1dafe4a8ad63774d",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"symfony/intl": "^5.4|^6.0",
"twig/twig": "^2.7|^3.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^5.4|^6.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Twig\\Extra\\Intl\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
}
],
"description": "A Twig extension for Intl",
"homepage": "https://twig.symfony.com",
"keywords": [
"intl",
"twig"
],
"support": {
"source": "https://github.com/twigphp/intl-extra/tree/v3.7.1"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2023-07-29T15:34:56+00:00"
},
{
"name": "twig/twig",
"version": "v3.7.1",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/a0ce373a0ca3bf6c64b9e3e2124aca502ba39554",
"reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3"
},
"require-dev": {
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Twig\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Twig Team",
"role": "Contributors"
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"role": "Project Founder"
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "https://twig.symfony.com",
"keywords": [
"templating"
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.7.1"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2023-08-28T11:09:02+00:00"
>>>>>>> support_for_custom_statement_designs
},
{
"name": "twilio/sdk",
@ -14528,6 +14746,7 @@
},
{
"name": "brianium/paratest",
<<<<<<< HEAD
"version": "v7.2.8",
"source": {
"type": "git",
@ -14538,6 +14757,18 @@
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/882b02d197328138686bb06ce7d8cbb98fc0a16c",
"reference": "882b02d197328138686bb06ce7d8cbb98fc0a16c",
=======
"version": "v7.2.7",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
"reference": "1526eb4fd195f65075456dee394d14742ae0a66c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/1526eb4fd195f65075456dee394d14742ae0a66c",
"reference": "1526eb4fd195f65075456dee394d14742ae0a66c",
>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@ -14607,7 +14838,11 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
<<<<<<< HEAD
"source": "https://github.com/paratestphp/paratest/tree/v7.2.8"
=======
"source": "https://github.com/paratestphp/paratest/tree/v7.2.7"
>>>>>>> support_for_custom_statement_designs
},
"funding": [
{
@ -14619,7 +14854,11 @@
"type": "paypal"
}
],
<<<<<<< HEAD
"time": "2023-10-04T13:38:04+00:00"
=======
"time": "2023-09-14T14:10:09+00:00"
>>>>>>> support_for_custom_statement_designs
},
{
"name": "composer/class-map-generator",
@ -15046,6 +15285,7 @@
},
{
"name": "friendsofphp/php-cs-fixer",
<<<<<<< HEAD
"version": "v3.34.1",
"source": {
"type": "git",
@ -15056,6 +15296,18 @@
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/98bf1b1068b4ceddbbc2a2b70b67a5e380add9e3",
"reference": "98bf1b1068b4ceddbbc2a2b70b67a5e380add9e3",
=======
"version": "v3.34.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "7c7a4ad2ed8fe50df3e25528218b13d383608f23"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7c7a4ad2ed8fe50df3e25528218b13d383608f23",
"reference": "7c7a4ad2ed8fe50df3e25528218b13d383608f23",
>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@ -15076,6 +15328,9 @@
"symfony/process": "^5.4 || ^6.0",
"symfony/stopwatch": "^5.4 || ^6.0"
},
"conflict": {
"stevebauman/unfinalize": "*"
},
"require-dev": {
"facile-it/paraunit": "^1.3 || ^2.0",
"justinrainbow/json-schema": "^5.2",
@ -15129,7 +15384,11 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
<<<<<<< HEAD
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.34.1"
=======
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.34.0"
>>>>>>> support_for_custom_statement_designs
},
"funding": [
{
@ -15137,7 +15396,11 @@
"type": "github"
}
],
<<<<<<< HEAD
"time": "2023-10-03T23:51:05+00:00"
=======
"time": "2023-09-29T15:34:26+00:00"
>>>>>>> support_for_custom_statement_designs
},
{
"name": "hamcrest/hamcrest-php",
@ -15848,6 +16111,7 @@
},
{
"name": "phpstan/phpstan",
<<<<<<< HEAD
"version": "1.10.37",
"source": {
"type": "git",
@ -15858,6 +16122,18 @@
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/058ba07e92f744d4dcf6061ae75283d0c6456f2e",
"reference": "058ba07e92f744d4dcf6061ae75283d0c6456f2e",
=======
"version": "1.10.36",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "ffa3089511121a672e62969404e4fddc753f9b15"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/ffa3089511121a672e62969404e4fddc753f9b15",
"reference": "ffa3089511121a672e62969404e4fddc753f9b15",
>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@ -15906,6 +16182,7 @@
"type": "tidelift"
}
],
<<<<<<< HEAD
"time": "2023-10-02T16:18:37+00:00"
},
{
@ -15920,6 +16197,22 @@
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/355324ca4980b8916c18b9db29f3ef484078f26e",
"reference": "355324ca4980b8916c18b9db29f3ef484078f26e",
=======
"time": "2023-09-29T14:07:45+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "10.1.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "56f33548fe522c8d82da7ff3824b42829d324364"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/56f33548fe522c8d82da7ff3824b42829d324364",
"reference": "56f33548fe522c8d82da7ff3824b42829d324364",
>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@ -15976,7 +16269,11 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
<<<<<<< HEAD
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.7"
=======
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.6"
>>>>>>> support_for_custom_statement_designs
},
"funding": [
{
@ -15984,7 +16281,11 @@
"type": "github"
}
],
<<<<<<< HEAD
"time": "2023-10-04T15:34:17+00:00"
=======
"time": "2023-09-19T04:59:03+00:00"
>>>>>>> support_for_custom_statement_designs
},
{
"name": "phpunit/php-file-iterator",
@ -17814,13 +18115,13 @@
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.1",
"php": "^8.1|^8.2",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*"
},
"platform-dev": {
"php": "^8.1"
"php": "^8.1|^8.2"
},
"plugin-api-version": "2.3.0"
}

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('clients', function (Blueprint $table) {
$table->string('classification')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('designs', function (Blueprint $table) {
$table->boolean('is_template')->default(false);
$table->text('entities')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
}
};

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('payments', function (Blueprint $table) {
$table->text('refund_meta')->nullable();
$table->unsignedInteger('category_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
}
};

View File

@ -5184,7 +5184,11 @@ $LANG = array(
'client_contact' => 'Client Contact',
'uncategorized' => 'Uncategorized',
'login_notification' => 'Login Notification',
'login_notification_help' => 'Sends an email notifying that a login has taken place.'
'login_notification_help' => 'Sends an email notifying that a login has taken place.',
'payment_refund_receipt' => 'Payment Refund Receipt # :number',
'payment_receipt' => 'Payment Receipt # :number',
'load_template_description' => 'The template will be applied to following:',
'run_template' => 'Run template',
);
return $LANG;

0
public/storage/.htaccess Normal file → Executable file
View File

View File

@ -388,7 +388,7 @@ $entity_images
];
tables.forEach((tableIdentifier) => {
document.getElementById(tableIdentifier).childElementCount === 0
document.getElementById(tableIdentifier) && document.getElementById(tableIdentifier).childElementCount === 0
? document.getElementById(tableIdentifier).style.display = 'none'
: '';
});

View File

@ -26,6 +26,7 @@ use App\Http\Controllers\ExportController;
use App\Http\Controllers\FilterController;
use App\Http\Controllers\ImportController;
use App\Http\Controllers\LogoutController;
use App\Http\Controllers\SearchController;
use App\Http\Controllers\StaticController;
use App\Http\Controllers\StripeController;
use App\Http\Controllers\TwilioController;
@ -73,6 +74,7 @@ use App\Http\Controllers\BankTransactionController;
use App\Http\Controllers\ClientStatementController;
use App\Http\Controllers\ExpenseCategoryController;
use App\Http\Controllers\HostedMigrationController;
use App\Http\Controllers\TemplatePreviewController;
use App\Http\Controllers\ConnectedAccountController;
use App\Http\Controllers\RecurringExpenseController;
use App\Http\Controllers\RecurringInvoiceController;
@ -111,7 +113,6 @@ use App\Http\Controllers\Reports\ClientContactReportController;
use App\Http\Controllers\Reports\PurchaseOrderReportController;
use App\Http\Controllers\Reports\RecurringInvoiceReportController;
use App\Http\Controllers\Reports\PurchaseOrderItemReportController;
use App\Http\Controllers\SearchController;
Route::group(['middleware' => ['throttle:api', 'api_secret_check']], function () {
Route::post('api/v1/signup', [AccountController::class, 'store'])->name('signup.submit');
@ -321,6 +322,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
Route::post('reports/tax_summary_report', TaxSummaryReportController::class);
Route::post('reports/user_sales_report', UserSalesReportController::class);
Route::post('reports/preview/{hash}', ReportPreviewController::class);
Route::post('templates/preview/{hash}', TemplatePreviewController::class);
Route::post('search', SearchController::class);
Route::resource('task_schedulers', TaskSchedulerController::class);

View File

@ -141,6 +141,7 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie
});
Route::get('phantom/{entity}/{invitation_key}', [Phantom::class, 'displayInvitation'])->middleware(['invite_db', 'phantom_secret'])->name('phantom_view');
Route::get('blade/', [Phantom::class, 'blade'])->name('blade');
Route::get('.env', function () {
})->middleware('throttle:honeypot');

View File

@ -14,6 +14,7 @@ namespace Tests\Feature;
use Tests\TestCase;
use App\Models\Design;
use Tests\MockAccountData;
use App\Factory\DesignFactory;
use App\Utils\Traits\MakesHash;
use App\Events\Design\DesignWasCreated;
use App\Events\Design\DesignWasDeleted;
@ -36,6 +37,8 @@ class DesignApiTest extends TestCase
public $id;
public $faker;
protected function setUp() :void
{
parent::setUp();
@ -49,6 +52,136 @@ class DesignApiTest extends TestCase
Model::reguard();
}
public function testFindInSetQueries()
{
$design = DesignFactory::create($this->company->id, $this->user->id);
$design->is_template = true;
$design->name = 'Test Template';
$design->entities = 'searchable,payment,quote';
$design->save();
$searchable = 'searchable';
$q = Design::query()
->where('is_template', true)
->whereRaw('FIND_IN_SET( ? ,entities)', [$searchable]);
$this->assertEquals(1, $q->count());
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/designs?entities=payment');
$response->assertStatus(200);
$arr = $response->json();
$this->assertCount(1, $arr['data']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/designs?entities=,,,3,3,3,');
$response->assertStatus(200);
$arr = $response->json();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/designs?entities=unsearchable');
$response->assertStatus(200);
$arr = $response->json();
$this->assertCount(0, $arr['data']);
$design = DesignFactory::create($this->company->id, $this->user->id);
$design->is_template = true;
$design->name = 'Test Template';
$design->entities = 'searchable,payment,quote';
$design->save();
$searchable = 'unsearchable';
$q = Design::query()
->where('is_template', true)
->whereRaw('FIND_IN_SET( ? ,entities)', [$searchable]);
$this->assertEquals(0, $q->count());
$design = DesignFactory::create($this->company->id, $this->user->id);
$design->is_template = true;
$design->name = 'Test Template';
$design->entities = 'searchable,payment,quote';
$design->save();
$searchable = 'searchable,payment';
$q = Design::query()
->where('is_template', true)
->whereRaw('FIND_IN_SET( ? ,entities)', [$searchable]);
$this->assertEquals(0, $q->count());
}
public function testDesignTemplates()
{
$design = DesignFactory::create($this->company->id, $this->user->id);
$design->is_template = true;
$design->name = 'Test Template';
$design->save();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/designs?template=true');
$response->assertStatus(200);
$arr = $response->json();
$this->assertCount(1, $arr['data']);
}
public function testDesignTemplatesExcluded()
{
$design = DesignFactory::create($this->company->id, $this->user->id);
$design->is_template = true;
$design->name = 'Test Template';
$design->save();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/designs?template=false');
$response->assertStatus(200);
$arr = $response->json();
$this->assertCount(11, $arr['data']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/designs');
$response->assertStatus(200);
$arr = $response->json();
$this->assertCount(12, $arr['data']);
}
public function testDesignPost()
{

View File

@ -271,16 +271,6 @@ class ReminderTest extends TestCase
$this->assertEquals(103, $fee->cost);
$this->assertEquals('Fee added '.now()->format('d/M/Y'), $fee->notes);
// $this->travelTo(now()->addHours(1));
// }
$this->travelBack();
}
@ -429,8 +419,8 @@ class ReminderTest extends TestCase
$next_send_date = Carbon::parse($this->invoice->next_send_date);
$calculatedReminderDate = Carbon::parse($this->invoice->due_date)->subDays(4)->addSeconds($this->invoice->client->timezone_offset());
nlog($next_send_date->format('Y-m-d h:i:s'));
nlog($calculatedReminderDate->format('Y-m-d h:i:s'));
// nlog($next_send_date->format('Y-m-d h:i:s'));
// nlog($calculatedReminderDate->format('Y-m-d h:i:s'));
$this->travelTo($calculatedReminderDate);
@ -451,7 +441,7 @@ class ReminderTest extends TestCase
$next_send_date = Carbon::parse($this->invoice->next_send_date);
nlog($next_send_date->format('Y-m-d h:i:s'));
// nlog($next_send_date->format('Y-m-d h:i:s'));
$calculatedReminderDate = Carbon::parse($this->invoice->due_date)->subDays(2)->addSeconds($this->invoice->client->timezone_offset());
$this->assertTrue($next_send_date->eq($calculatedReminderDate));
@ -470,7 +460,7 @@ class ReminderTest extends TestCase
$calculatedReminderDate = Carbon::parse($this->invoice->due_date)->addDays(3)->addSeconds($this->invoice->client->timezone_offset());
$this->assertTrue($next_send_date->eq($calculatedReminderDate));
nlog($next_send_date->format('Y-m-d h:i:s'));
// nlog($next_send_date->format('Y-m-d h:i:s'));
}
public function testReminderQueryCatchesDate()

View File

@ -0,0 +1,708 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature\Template;
use Tests\TestCase;
use App\Utils\Ninja;
use App\Utils\Number;
use App\Models\Credit;
use App\Models\Design;
use App\Models\Invoice;
use App\Models\Payment;
use App\Utils\HtmlEngine;
use Tests\MockAccountData;
use App\Utils\Traits\MakesDates;
use App\Services\PdfMaker\PdfMaker;
use Illuminate\Support\Facades\App;
use App\Jobs\Entity\CreateEntityPdf;
use App\Services\Template\TemplateService;
use App\Services\PdfMaker\Design as PdfDesignModel;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* @test
* @covers
*/
class TemplateTest extends TestCase
{
use DatabaseTransactions;
use MockAccountData;
use MakesDates;
private string $body = '
<ninja>
$company.name
<table class="min-w-full text-left text-sm font-light">
<thead class="border-b font-medium dark:border-neutral-500">
<tr class="text-sm leading-normal">
<th scope="col" class="px-6 py-4">Item #</th>
<th scope="col" class="px-6 py-4">Description</th>
<th scope="col" class="px-6 py-4">Ordered</th>
<th scope="col" class="px-6 py-4">Delivered</th>
<th scope="col" class="px-6 py-4">Outstanding</th>
</tr>
</thead>
<tbody>
{% for entity in invoices %}
{% for item in entity.line_items|filter(item => item.type_id == "1") %}
<tr class="border-b dark:border-neutral-500">
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ item.product_key }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ item.notes }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ item.quantity }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ item.quantity }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium">0</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</ninja>
';
private string $nested_body = '
<ninja>
$company.name
<table class="min-w-full text-left text-sm font-light">
<thead class="border-b font-medium dark:border-neutral-500">
<tr class="text-sm leading-normal">
<th scope="col" class="px-6 py-4">Item #</th>
<th scope="col" class="px-6 py-4">Description</th>
<th scope="col" class="px-6 py-4">Ordered</th>
<th scope="col" class="px-6 py-4">Delivered</th>
<th scope="col" class="px-6 py-4">Outstanding</th>
</tr>
</thead>
<tbody>
{% for entity in invoices %}
Client Name: {{ entity.client.name }}
Client Name with variables = $client.name
{% for item in entity.line_items|filter(item => item.type_id == "1") %}
<tr class="border-b dark:border-neutral-500">
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ item.product_key }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ item.notes }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ item.quantity }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ item.quantity }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium">0</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</ninja>
';
private string $payments_body = '
CoName: $company.name
ClName: $client.name
InNumber: $invoice.number
<ninja>
CoName: $company.name
ClName: $client.name
InNumber: $invoice.number
<table class="min-w-full text-left text-sm font-light">
<thead class="border-b font-medium dark:border-neutral-500">
<tr class="text-sm leading-normal">
<th scope="col" class="px-6 py-4">Invoice #</th>
<th scope="col" class="px-6 py-4">Date</th>
<th scope="col" class="px-6 py-4">Due Date</th>
<th scope="col" class="px-6 py-4">Total</th>
<th scope="col" class="px-6 py-4">Transaction</th>
<th scope="col" class="px-6 py-4">Outstanding</th>
</tr>
</thead>
<tbody>
{% for invoice in invoices %}
<tr class="border-b dark:border-neutral-500">
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ invoice.number }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ invoice.date }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ invoice.due_date }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ invoice.amount }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium"></td>
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ invoice.balance }}</td>
</tr>
{% for payment in invoice.payments|filter(payment => payment.is_deleted == false) %}
{% for pivot in payment.paymentables %}
<tr class="border-b dark:border-neutral-500">
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ payment.number }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ payment.date }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium"></td>
<td class="whitespace-nowrap px-6 py-4 font-medium">
{% if pivot.amount_raw > 0 %}
{{ pivot.amount }} - {{ payment.type.name }}
{% else %}
({{ pivot.refunded }})
{% endif %}
</td>
<td class="whitespace-nowrap px-6 py-4 font-medium"></td>
<td class="whitespace-nowrap px-6 py-4 font-medium"></td>
</tr>
{% endfor %}
{% endfor %}
{% endfor%}
</tbody>
</table>
</ninja>
';
protected function setUp() :void
{
parent::setUp();
$this->makeTestData();
$this->withoutMiddleware(
ThrottleRequests::class
);
}
public function testDataMaps()
{
$start = microtime(true);
Invoice::factory()->count(10)->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'status_id' => Invoice::STATUS_SENT,
'amount' => 100,
'balance' => 100,
]);
$invoices = Invoice::orderBy('id','desc')->where('client_id', $this->client->id)->take(10)->get()->map(function($c){
return $c->service()->markSent()->applyNumber()->save();
})->map(function ($i){
return ['invoice_id' => $i->hashed_id, 'amount' => rand(0, $i->balance)];
})->toArray();
Credit::factory()->count(2)->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'status_id' => Invoice::STATUS_SENT,
'amount' => 50,
'balance' => 50,
]);
$credits = Credit::orderBy('id', 'desc')->where('client_id', $this->client->id)->take(2)->get()->map(function($c){
return $c->service()->markSent()->applyNumber()->save();
})->map(function ($i) {
return ['credit_id' => $i->hashed_id, 'amount' => rand(0, $i->balance)];
})->toArray();
$data = [
'invoices' => $invoices,
'credits' => $credits,
'date' => now()->format('Y-m-d'),
'client_id' => $this->client->hashed_id,
'transaction_reference' => 'My Batch Payment',
'type_id' => "5",
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/payments/', $data);
$response->assertStatus(200);
$arr = $response->json();
$start = microtime(true);
$p = Payment::with('client','invoices','paymentables','credits')
->where('id', $this->decodePrimaryKey($arr['data']['id']))
->cursor()
->map(function ($payment){
$this->transformPayment($payment);
})->toArray();
nlog("end payments = " . microtime(true) - $start);
$this->assertIsArray($data);
$start = microtime(true);
\DB::enableQueryLog();
$invoices = Invoice::with('client','payments.client','payments.paymentables','payments.credits','credits.client')
->orderBy('id','desc')
->where('client_id', $this->client->id)
->take(10)
->get()
->map(function($invoice){
$payments = [];
$payments = $invoice->payments->map(function ($payment){
// nlog(microtime(true));
return $this->transformPayment($payment);
})->toArray();
return [
'amount' => Number::formatMoney($invoice->amount, $invoice->client),
'balance' => Number::formatMoney($invoice->balance, $invoice->client),
'balance_raw' => $invoice->balance,
'number' => $invoice->number ?: '',
'discount' => $invoice->discount,
'po_number' => $invoice->po_number ?: '',
'date' => $this->translateDate($invoice->date, $invoice->client->date_format(), $invoice->client->locale()),
'last_sent_date' => $this->translateDate($invoice->last_sent_date, $invoice->client->date_format(), $invoice->client->locale()),
'next_send_date' => $this->translateDate($invoice->next_send_date, $invoice->client->date_format(), $invoice->client->locale()),
'due_date' => $this->translateDate($invoice->due_date, $invoice->client->date_format(), $invoice->client->locale()),
'terms' => $invoice->terms ?: '',
'public_notes' => $invoice->public_notes ?: '',
'private_notes' => $invoice->private_notes ?: '',
'uses_inclusive_taxes' => (bool) $invoice->uses_inclusive_taxes,
'tax_name1' => $invoice->tax_name1 ?? '',
'tax_rate1' => (float) $invoice->tax_rate1,
'tax_name2' => $invoice->tax_name2 ?? '',
'tax_rate2' => (float) $invoice->tax_rate2,
'tax_name3' => $invoice->tax_name3 ?? '',
'tax_rate3' => (float) $invoice->tax_rate3,
'total_taxes' => Number::formatMoney($invoice->total_taxes, $invoice->client),
'total_taxes_raw' => $invoice->total_taxes,
'is_amount_discount' => (bool) $invoice->is_amount_discount ?? false,
'footer' => $invoice->footer ?? '',
'partial' => $invoice->partial ?? 0,
'partial_due_date' => $this->translateDate($invoice->partial_due_date, $invoice->client->date_format(), $invoice->client->locale()),
'custom_value1' => (string) $invoice->custom_value1 ?: '',
'custom_value2' => (string) $invoice->custom_value2 ?: '',
'custom_value3' => (string) $invoice->custom_value3 ?: '',
'custom_value4' => (string) $invoice->custom_value4 ?: '',
'custom_surcharge1' => (float) $invoice->custom_surcharge1,
'custom_surcharge2' => (float) $invoice->custom_surcharge2,
'custom_surcharge3' => (float) $invoice->custom_surcharge3,
'custom_surcharge4' => (float) $invoice->custom_surcharge4,
'exchange_rate' => (float) $invoice->exchange_rate,
'custom_surcharge_tax1' => (bool) $invoice->custom_surcharge_tax1,
'custom_surcharge_tax2' => (bool) $invoice->custom_surcharge_tax2,
'custom_surcharge_tax3' => (bool) $invoice->custom_surcharge_tax3,
'custom_surcharge_tax4' => (bool) $invoice->custom_surcharge_tax4,
'line_items' => $invoice->line_items ?: (array) [],
'reminder1_sent' => $this->translateDate($invoice->reminder1_sent, $invoice->client->date_format(), $invoice->client->locale()),
'reminder2_sent' => $this->translateDate($invoice->reminder2_sent, $invoice->client->date_format(), $invoice->client->locale()),
'reminder3_sent' => $this->translateDate($invoice->reminder3_sent, $invoice->client->date_format(), $invoice->client->locale()),
'reminder_last_sent' => $this->translateDate($invoice->reminder_last_sent, $invoice->client->date_format(), $invoice->client->locale()),
'paid_to_date' => Number::formatMoney($invoice->paid_to_date, $invoice->client),
'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled,
'client' => [
'name' => $invoice->client->present()->name(),
'balance' => $invoice->client->balance,
'payment_balance' => $invoice->client->payment_balance,
'credit_balance' => $invoice->client->credit_balance,
],
'payments' => $payments,
];
});
$queries = \DB::getQueryLog();
$count = count($queries);
nlog("query count = {$count}");
$x = $invoices->toArray();
// nlog(json_encode($x));
// nlog(json_encode(htmlspecialchars(json_encode($x), ENT_QUOTES, 'UTF-8')));
// nlog($invoices->toJson());
$this->assertIsArray($invoices->toArray());
nlog("end invoices = " . microtime(true) - $start);
}
private function transformPayment(Payment $payment): array
{
$data = [];
$credits = $payment->credits->map(function ($credit) use ($payment) {
return [
'credit' => $credit->number,
'amount_raw' => $credit->pivot->amount,
'refunded_raw' => $credit->pivot->refunded,
'net_raw' => $credit->pivot->amount - $credit->pivot->refunded,
'amount' => Number::formatMoney($credit->pivot->amount, $payment->client),
'refunded' => Number::formatMoney($credit->pivot->refunded, $payment->client),
'net' => Number::formatMoney($credit->pivot->amount - $credit->pivot->refunded, $payment->client),
'is_credit' => true,
'created_at' => $this->translateDate($credit->pivot->created_at, $payment->client->date_format(), $payment->client->locale()),
'updated_at' => $this->translateDate($credit->pivot->updated_at, $payment->client->date_format(), $payment->client->locale()),
'timestamp' => $credit->pivot->created_at->timestamp,
];
});
$pivot = $payment->invoices->map(function ($invoice) use ($payment) {
return [
'invoice' => $invoice->number,
'amount_raw' => $invoice->pivot->amount,
'refunded_raw' => $invoice->pivot->refunded,
'net_raw' => $invoice->pivot->amount - $invoice->pivot->refunded,
'amount' => Number::formatMoney($invoice->pivot->amount, $payment->client),
'refunded' => Number::formatMoney($invoice->pivot->refunded, $payment->client),
'net' => Number::formatMoney($invoice->pivot->amount - $invoice->pivot->refunded, $payment->client),
'is_credit' => false,
'created_at' => $this->translateDate($invoice->pivot->created_at, $payment->client->date_format(), $payment->client->locale()),
'updated_at' => $this->translateDate($invoice->pivot->updated_at, $payment->client->date_format(), $payment->client->locale()),
'timestamp' => $invoice->pivot->created_at->timestamp,
];
})->merge($credits)->sortBy('timestamp')->toArray();
return [
'status' => $payment->stringStatus($payment->status_id),
'badge' => $payment->badgeForStatus($payment->status_id),
'amount' => Number::formatMoney($payment->amount, $payment->client),
'applied' => Number::formatMoney($payment->applied, $payment->client),
'balance' => Number::formatMoney(($payment->amount - $payment->refunded - $payment->applied), $payment->client),
'refunded' => Number::formatMoney($payment->refunded, $payment->client),
'amount_raw' => $payment->amount,
'applied_raw' => $payment->applied,
'refunded_raw' => $payment->refunded,
'balance_raw' => ($payment->amount - $payment->refunded - $payment->applied),
'date' => $this->translateDate($payment->date, $payment->client->date_format(), $payment->client->locale()),
'method' => $payment->translatedType(),
'currency' => $payment->currency->code,
'exchange_rate' => $payment->exchange_rate,
'transaction_reference' => $payment->transaction_reference,
'is_manual' => $payment->is_manual,
'number' => $payment->number,
'custom_value1' => $payment->custom_value1 ?? '',
'custom_value2' => $payment->custom_value2 ?? '',
'custom_value3' => $payment->custom_value3 ?? '',
'custom_value4' => $payment->custom_value4 ?? '',
'client' => [
'name' => $payment->client->present()->name(),
'balance' => $payment->client->balance,
'payment_balance' => $payment->client->payment_balance,
'credit_balance' => $payment->client->credit_balance,
],
'paymentables' => $pivot,
];
return $data;
}
public function testVariableResolutionViaTransformersForPaymentsInStatements()
{
Invoice::factory()->count(20)->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'status_id' => Invoice::STATUS_SENT,
'amount' => 100,
'balance' => 100,
]);
$i = Invoice::orderBy('id','desc')
->where('client_id', $this->client->id)
->where('status_id', 2)
->cursor()
->each(function ($i){
$i->service()->applyPaymentAmount(random_int(1,100));
});
$invoices = Invoice::withTrashed()
->with('payments.type')
->where('is_deleted', false)
->where('company_id', $this->client->company_id)
->where('client_id', $this->client->id)
->whereIn('status_id', [2,3,4])
->orderBy('due_date', 'ASC')
->orderBy('date', 'ASC')
->cursor();
$invoices->each(function ($i){
$rand = [1,2,4,5,6,7,8,9,10,11,12,13,14,15,16,17,24,25,32,49,50];
$i->payments()->each(function ($p) use ($rand){
shuffle($rand);
$p->type_id = $rand[0];
$p->save();
});
});
$design_model = Design::find(2);
$replicated_design = $design_model->replicate();
$replicated_design->company_id = $this->company->id;
$replicated_design->user_id = $this->user->id;
$design = $replicated_design->design;
$design->body .= $this->payments_body;
$replicated_design->design = $design;
$replicated_design->is_custom = true;
$replicated_design->is_template =true;
$replicated_design->entities = 'client';
$replicated_design->save();
$data['invoices'] = $invoices;
$ts = $replicated_design->service()->build($data);
// nlog("results = ");
// nlog($ts->getHtml());
$this->assertNotNull($ts->getHtml());
}
public function testDoubleEntityNestedDataTemplateServiceBuild()
{
$design_model = Design::find(2);
$replicated_design = $design_model->replicate();
$replicated_design->company_id = $this->company->id;
$replicated_design->user_id = $this->user->id;
$design = $replicated_design->design;
$design->body .= $this->nested_body;
$replicated_design->design = $design;
$replicated_design->is_custom = true;
$replicated_design->save();
$i2 = Invoice::factory()
->for($this->client)
->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'status_id' => Invoice::STATUS_SENT,
'design_id' => $replicated_design->id,
'balance' => 100,
]);
$data = [];
$data['invoices'] = collect([$this->invoice, $i2]);
$ts = $replicated_design->service()->build($data);
// nlog("results = ");
// nlog($ts->getHtml());
$this->assertNotNull($ts->getHtml());
}
public function testDoubleEntityTemplateServiceBuild()
{
$design_model = Design::find(2);
$replicated_design = $design_model->replicate();
$replicated_design->company_id = $this->company->id;
$replicated_design->user_id = $this->user->id;
$design = $replicated_design->design;
$design->body .= $this->body;
$replicated_design->design = $design;
$replicated_design->is_custom = true;
$replicated_design->save();
$i2 = Invoice::factory()
->for($this->client)
->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'status_id' => Invoice::STATUS_SENT,
'design_id' => $replicated_design->id,
'balance' => 100,
]);
$data = [];
$data['invoices'] = collect([$this->invoice, $i2]);
$ts = $replicated_design->service()->build($data);
// nlog("results = ");
// nlog($ts->getHtml());
$this->assertNotNull($ts->getHtml());
}
public function testTemplateServiceBuild()
{
$design_model = Design::find(2);
$replicated_design = $design_model->replicate();
$replicated_design->company_id = $this->company->id;
$replicated_design->user_id = $this->user->id;
$design = $replicated_design->design;
$design->body .= $this->body;
$replicated_design->design = $design;
$replicated_design->is_custom = true;
$replicated_design->save();
$data = [];
$data['invoices'] = collect([$this->invoice]);
$ts = $replicated_design->service()->build($data);
// nlog("results = ");
// nlog($ts->getHtml());
$this->assertNotNull($ts->getHtml());
}
public function testTemplateService()
{
$design_model = Design::find(2);
$replicated_design = $design_model->replicate();
$replicated_design->company_id = $this->company->id;
$replicated_design->user_id = $this->user->id;
$design = $replicated_design->design;
$design->body .= $this->body;
$replicated_design->design = $design;
$replicated_design->is_custom = true;
$replicated_design->save();
$this->assertNotNull($replicated_design->service());
$this->assertInstanceOf(TemplateService::class, $replicated_design->service());
}
public function testTimingOnCleanDesign()
{
$design_model = Design::find(2);
$replicated_design = $design_model->replicate();
$replicated_design->company_id = $this->company->id;
$replicated_design->user_id = $this->user->id;
$design = $replicated_design->design;
$design->body .= $this->body;
$replicated_design->design = $design;
$replicated_design->is_custom = true;
$replicated_design->save();
$entity_obj = \App\Models\Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'status_id' => Invoice::STATUS_SENT,
'design_id' => $replicated_design->id,
]);
$i = \App\Models\InvoiceInvitation::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'invoice_id' => $entity_obj->id,
'client_contact_id' => $this->client->contacts->first()->id,
]);
$start = microtime(true);
$pdf = (new CreateEntityPdf($i))->handle();
$end = microtime(true);
$this->assertNotNull($pdf);
nlog("Twig + PDF Gen Time: " . $end-$start);
}
public function testStaticPdfGeneration()
{
$start = microtime(true);
$pdf = (new CreateEntityPdf($this->invoice->invitations->first()))->handle();
$end = microtime(true);
$this->assertNotNull($pdf);
nlog("Plain PDF Gen Time: " . $end-$start);
}
public function testTemplateGeneration()
{
$entity_obj = $this->invoice;
$design = new Design();
$design->design = json_decode(json_encode($this->invoice->company->settings->pdf_variables), true);
$design->name = 'test';
$design->is_active = true;
$design->is_template = true;
$design->is_custom = true;
$design->user_id = $this->invoice->user_id;
$design->company_id = $this->invoice->company_id;
$design_object = new \stdClass;
$design_object->includes = '';
$design_object->header = '';
$design_object->body = $this->body;
$design_object->product = '';
$design_object->task = '';
$design_object->footer = '';
$design->design = $design_object;
$design->save();
$start = microtime(true);
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($entity_obj->client->locale());
$t->replace(Ninja::transformTranslations($entity_obj->client->getMergedSettings()));
$html = new HtmlEngine($entity_obj->invitations()->first());
$options = [
'custom_partials' => json_decode(json_encode($design->design), true),
];
$template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options);
$variables = $html->generateLabelsAndValues();
$state = [
'template' => $template->elements([
'client' => $entity_obj->client,
'entity' => $entity_obj,
'pdf_variables' => (array) $entity_obj->company->settings->pdf_variables,
'$product' => $design->design->product,
'variables' => $variables,
]),
'variables' => $variables,
'options' => [
'all_pages_header' => $entity_obj->client->getSetting('all_pages_header'),
'all_pages_footer' => $entity_obj->client->getSetting('all_pages_footer'),
'client' => $entity_obj->client,
'entity' => $entity_obj,
'variables' => $variables,
],
'process_markdown' => $entity_obj->client->company->markdown_enabled,
];
$maker = new PdfMaker($state);
$maker
->design($template)
->build();
$html = $maker->getCompiledHTML(true);
$end = microtime(true);
$this->assertNotNull($html);
$this->assertStringContainsStringIgnoringCase($this->company->settings->name, $html);
nlog("Twig Solo Gen Time: ". $end - $start);
}
}