1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 05:02:36 +01:00

Updates for twig templates

This commit is contained in:
David Bomba 2023-09-25 13:19:08 +10:00
parent 2a40658222
commit 487ca15749
14 changed files with 304 additions and 39 deletions

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

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

@ -45,7 +45,7 @@ class StoreDesignRequest extends Request
'design.footer' => 'required|min:1',
'design.includes' => 'required|min:1',
'is_template' => 'sometimes|boolean',
'entities' => 'sometimes|string'
'entities' => 'sometimes|string|nullable'
];
}

View File

@ -35,7 +35,7 @@ class UpdateDesignRequest extends Request
{
return [
'is_template' => 'sometimes|boolean',
'entities' => 'sometimes|string'
'entities' => 'sometimes|string|nullable'
];
}

View File

@ -65,8 +65,6 @@ class UpdateInvoiceRequest extends Request
$rules['is_amount_discount'] = ['boolean'];
nlog($this->partial);
$rules['line_items'] = 'array';
$rules['discount'] = 'sometimes|numeric';
$rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())];

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

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

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

@ -29,11 +29,12 @@ use App\Utils\Traits\Pdf\PdfMaker as PdfMakerTrait;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use App\Models\Credit;
use App\Utils\Traits\MakesHash;
class Statement
{
use PdfMakerTrait;
use MakesHash;
/**
* @var Invoice|Payment|null
*/
@ -88,21 +89,44 @@ class Statement
'process_markdown' => $this->entity->client->company->markdown_enabled,
];
$maker = new PdfMaker($state);
if($this->options['template'] ?? false){
$maker
->design($template)
->build();
$template = Design::where('id', $this->decodePrimaryKey($this->options['template']))
->where('company_id', $this->client->company_id)
->first();
$pdf = null;
$ts = $template->service()->build([
'client' => $this->client,
'entity' => $this->entity,
'variables' => $variables,
'invoices' => $this->getInvoices(),
'payments' => $this->getPayments(),
'credits' => $this->getCredits(),
'aging' => $this->getAging(),
]);
$html = $ts->getHtml();
}
else {
$maker = new PdfMaker($state);
$maker
->design($template)
->build();
$pdf = null;
$html = $maker->getCompiledHTML(true);
}
try {
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
$pdf = (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
$pdf = (new Phantom)->convertHtmlToPdf($html);
} elseif (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') {
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$pdf = (new NinjaPdf())->build($html);
} else {
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
$pdf = $this->makePdf(null, null, $html);
}
} catch (\Exception $e) {
nlog(print_r($e->getMessage(), 1));

View File

@ -20,10 +20,12 @@ use App\Models\Payment;
use App\Models\Project;
use App\Utils\HtmlEngine;
use League\Fractal\Manager;
use App\Models\ClientContact;
use App\Models\PurchaseOrder;
use App\Utils\VendorHtmlEngine;
use App\Utils\PaymentHtmlEngine;
use Illuminate\Support\Collection;
use Twig\Extra\Intl\IntlExtension;
use App\Transformers\TaskTransformer;
use App\Transformers\QuoteTransformer;
use App\Transformers\CreditTransformer;
@ -91,7 +93,7 @@ class TemplateService
{
$data = $this->preProcessDataBlocks($data);
$replacements = [];
nlog($data);
$contents = $this->document->getElementsByTagName('ninja');
foreach ($contents as $content) {
@ -103,10 +105,13 @@ class TemplateService
$string_extension = new \Twig\Extension\StringLoaderExtension();
$twig->addExtension($string_extension);
$twig->addExtension(new IntlExtension());
$template = $twig->createTemplate(html_entity_decode($template));
$template = $template->render($data);
nlog($template);
$f = $this->document->createDocumentFragment();
$f->appendXML($template);
$replacements[] = $f;
@ -228,67 +233,92 @@ class TemplateService
private function processInvoices($invoices): array
{
$it = new InvoiceTransformer();
$it->setDefaultIncludes(['client']);
$it->setDefaultIncludes(['client','payments']);
$manager = new Manager();
// $manager->setSerializer(new JsonApiSerializer());
$resource = new \League\Fractal\Resource\Collection($invoices, $it, Invoice::class);
$i = $manager->createData($resource)->toArray();
return $i['data'];
$manager->parseIncludes(['client','payments','payments.type']);
$resource = new \League\Fractal\Resource\Collection($invoices, $it, null);
$invoices = $manager->createData($resource)->toArray();
// nlog($invoices);
foreach($invoices['data'] as $key => $invoice)
{
$invoices['data'][$key]['client'] = $invoice['client']['data'] ?? [];
$invoices['data'][$key]['client']['contacts'] = $invoice['client']['data']['contacts']['data'] ?? [];
$invoices['data'][$key]['payments'] = $invoice['payments']['data'] ?? [];
if($invoice['payments']['data'] ?? false) {
foreach($invoice['payments']['data'] as $keyx => $payment) {
$invoices['data'][$key]['payments'][$keyx]['paymentables']= $payment['paymentables']['data'] ?? [];
}
}
}
return $invoices['data'];
}
private function processQuotes($quotes): Collection
private function processQuotes($quotes): array
{
$it = new QuoteTransformer();
$it->setDefaultIncludes(['client']);
$manager = new Manager();
$manager->setSerializer(new ArraySerializer());
$resource = new \League\Fractal\Resource\Collection($quotes, $it, Quote::class);
$i = $manager->createData($resource)->toArray();
return $i['data'];
$i['client']['contacts'] = $i['client']['contacts'][ClientContact::class];
return $i[Quote::class];
}
private function processCredits($credits): Collection
private function processCredits($credits): array
{
$it = new CreditTransformer();
$it->setDefaultIncludes(['client']);
$manager = new Manager();
$manager->setSerializer(new ArraySerializer());
$resource = new \League\Fractal\Resource\Collection($credits, $it, Credit::class);
$i = $manager->createData($resource)->toArray();
return $i['data'];
return $i[Credit::class];
}
private function processPayments($payments): Collection
private function processPayments($payments): array
{
$it = new PaymentTransformer();
$it->setDefaultIncludes(['client','invoices','paymentables']);
$manager = new Manager();
$manager->setSerializer(new ArraySerializer());
$resource = new \League\Fractal\Resource\Collection($payments, $it, Payment::class);
$i = $manager->createData($resource)->toArray();
return $i['data'];
return $i[Payment::class];
}
private function processTasks($tasks): Collection
private function processTasks($tasks): array
{
$it = new TaskTransformer();
$it->setDefaultIncludes(['client','tasks','project','invoice']);
$manager = new Manager();
$manager->setSerializer(new ArraySerializer());
$resource = new \League\Fractal\Resource\Collection($tasks, $it, Task::class);
$i = $manager->createData($resource)->toArray();
return $i['data'];
return $i[Task::class];
}
private function processProjects($projects): Collection
private 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['data'];
return $i[Project::class];
}
@ -298,9 +328,10 @@ class TemplateService
$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['data'];
return $i[PurchaseOrder::class];
}
}

View File

@ -32,6 +32,7 @@ class PaymentTransformer extends EntityTransformer
protected array $availableIncludes = [
'client',
'invoices',
'type',
];
public function __construct($serializer = null)
@ -69,6 +70,13 @@ class PaymentTransformer extends EntityTransformer
return $this->includeCollection($payment->documents, $transformer, Document::class);
}
public function includeType(Payment $payment)
{
return [
'type' => $payment->type->translatedType() ?? '',
];
}
public function transform(Payment $payment)
{
return [

View File

@ -94,6 +94,7 @@
"symfony/mailgun-mailer": "^6.1",
"symfony/postmark-mailer": "^6.1",
"turbo124/beacon": "^1.5",
"twig/intl-extra": "^3.7",
"twig/twig": "^3",
"twilio/sdk": "^6.40",
"webpatser/laravel-countries": "dev-master#75992ad",

66
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "08bc4729962b495b68162a069269f74f",
"content-hash": "0e0f7606a875b132577ee735309b1247",
"packages": [
{
"name": "afosto/yaac",
@ -13864,6 +13864,70 @@
},
"time": "2023-09-24T07:20:04+00:00"
},
{
"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",

View File

@ -101,6 +101,59 @@ class TemplateTest extends TestCase
';
private string $payments_body = '
<ninja>
<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|format_currency("EUR") }}</td>
<td class="whitespace-nowrap px-6 py-4 font-medium"></td>
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ invoice.balance|format_currency("EUR") }}</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 > 0 %}
{{ pivot.amount|format_currency("EUR") }} - {{ payment.type.name }}
{% else %}
({{ pivot.refunded|format_currency("EUR") }})
{% 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();
@ -113,6 +166,67 @@ class TemplateTest extends TestCase
}
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();
$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);