Merge pull request #4266 from beganovich/v5-tasks-table

(v5) Support for tasks on PDFs
This commit is contained in:
David Bomba 2020-11-05 07:14:13 +11:00 committed by GitHub
commit d861f448ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 141 additions and 54 deletions

View File

@ -624,8 +624,8 @@ class CompanySettings extends BaseSettings
'task_columns' =>[ 'task_columns' =>[
'$task.product_key', '$task.product_key',
'$task.notes', '$task.notes',
'$task.cost', '$task.rate',
'$task.quantity', '$task.hours',
'$task.discount', '$task.discount',
'$task.tax', '$task.tax',
'$task.line_total', '$task.line_total',

View File

@ -139,7 +139,7 @@ class CreateEntityPdf implements ShouldQueue
'client' => $this->entity->client, 'client' => $this->entity->client,
'entity' => $this->entity, 'entity' => $this->entity,
'pdf_variables' => (array) $this->entity->company->settings->pdf_variables, 'pdf_variables' => (array) $this->entity->company->settings->pdf_variables,
'products' => $design->design->product, '$product' => $design->design->product,
]), ]),
'variables' => $html->generateLabelsAndValues(), 'variables' => $html->generateLabelsAndValues(),
'options' => [ 'options' => [

View File

@ -133,7 +133,7 @@ class ActivityRepository extends BaseRepository
'client' => $entity->client, 'client' => $entity->client,
'entity' => $entity, 'entity' => $entity,
'pdf_variables' => (array) $entity->company->settings->pdf_variables, 'pdf_variables' => (array) $entity->company->settings->pdf_variables,
'products' => $design->design->product, '$product' => $design->design->product,
]), ]),
'variables' => $html->generateLabelsAndValues(), 'variables' => $html->generateLabelsAndValues(),
'options' => [ 'options' => [

View File

@ -106,9 +106,13 @@ class Design extends BaseDesign
'id' => 'product-table', 'id' => 'product-table',
'elements' => $this->productTable(), 'elements' => $this->productTable(),
], ],
'product-table-footer' => [ 'task-table' => [
'id' => 'product-table-footer', 'id' => 'task-table',
'elements' => $this->tableFooter(), 'elements' => $this->taskTable(),
],
'table-totals' => [
'id' => 'table-totals',
'elements' => $this->tableTotals(),
], ],
'footer-elements' => [ 'footer-elements' => [
'id' => 'footer', 'id' => 'footer',
@ -188,47 +192,95 @@ class Design extends BaseDesign
return $elements; return $elements;
} }
/**
* Parent method for building products table.
*
* @return array
*/
public function productTable(): array public function productTable(): array
{ {
$product_items = collect($this->entity->line_items)->filter(function ($item) {
return $item->type_id == 1;
});
if ($product_items->count() == 0) {
return [];
}
return [ return [
['element' => 'thead', 'elements' => $this->buildTableHeader()], ['element' => 'thead', 'elements' => $this->buildTableHeader('product')],
['element' => 'tbody', 'elements' => $this->buildTableBody()], ['element' => 'tbody', 'elements' => $this->buildTableBody('$product')],
]; ];
} }
public function buildTableHeader(): array /**
* Parent method for building tasks table.
*
* @return array
*/
public function taskTable(): array
{ {
$this->processTaxColumns(); $task_items = collect($this->entity->line_items)->filter(function ($item) {
return $item->type_id = 2;
});
if ($task_items->count() == 0) {
return [];
}
return [
['element' => 'thead', 'elements' => $this->buildTableHeader('task')],
['element' => 'tbody', 'elements' => $this->buildTableBody('$task')],
];
}
/**
* Generate the structure of table headers. (<thead/>)
*
* @param string $type "product" or "task"
* @return array
*/
public function buildTableHeader(string $type): array
{
$this->processTaxColumns($type);
$elements = []; $elements = [];
foreach ($this->context['pdf_variables']["{$this->type}_columns"] as $column) { foreach ($this->context['pdf_variables']["{$type}_columns"] as $column) {
$elements[] = ['element' => 'th', 'content' => $column . '_label']; $elements[] = ['element' => 'th', 'content' => $column . '_label'];
} }
return $elements; return $elements;
} }
public function buildTableBody(): array /**
* Generate the structure of table body. (<tbody/>)
*
* @param string $type "$product" or "$task"
* @return array
*/
public function buildTableBody(string $type): array
{ {
$elements = []; $elements = [];
$items = $this->transformLineItems($this->entity->line_items); $items = $this->transformLineItems($this->entity->line_items, $type);
if (count($items) == 0) { if (count($items) == 0) {
return []; return [];
} }
info($this->context);
foreach ($items as $row) { foreach ($items as $row) {
$element = ['element' => 'tr', 'elements' => []]; $element = ['element' => 'tr', 'elements' => []];
if ( if (
isset($this->context['products']) && array_key_exists($type, $this->context) &&
!empty($this->context['products']) && !empty($this->context[$type]) &&
!is_null($this->context['products']) !is_null($this->context[$type])
) { ) {
$document = new DOMDocument(); $document = new DOMDocument();
$document->loadHTML($this->context['products'], LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); $document->loadHTML($this->context[$type], LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$td = $document->getElementsByTagName('tr')->item(0); $td = $document->getElementsByTagName('tr')->item(0);
@ -246,10 +298,22 @@ class Design extends BaseDesign
} }
} }
} else { } else {
foreach ($this->context['pdf_variables']["{$this->type}_columns"] as $key => $cell) { $_type = Str::startsWith($type, '$') ? ltrim($type, '$') : $type;
foreach ($this->context['pdf_variables']["{$_type}_columns"] as $key => $cell) {
// We want to keep aliases like these:
// $task.cost => $task.rate
// $task.quantity => $task.hours
if ($cell == '$task.rate') {
$element['elements'][] = ['element' => 'td', 'content' => $row['$task.cost']];
} else if ($cell == '$task.hours') {
$element['elements'][] = ['element' => 'td', 'content' => $row['$task.quantity']];
} else {
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell]]; $element['elements'][] = ['element' => 'td', 'content' => $row[$cell]];
} }
} }
}
$elements[] = $element; $elements[] = $element;
} }
@ -257,13 +321,13 @@ class Design extends BaseDesign
return $elements; return $elements;
} }
public function tableFooter() public function tableTotals(): array
{ {
$variables = $this->context['pdf_variables']['total_columns']; $variables = $this->context['pdf_variables']['total_columns'];
$elements = [ $elements = [
['element' => 'div', 'elements' => [ ['element' => 'div', 'elements' => [
['element' => 'span', 'content' => '$entity.public_notes', 'properties' => ['data-element' => 'product-table-public-notes-label']], ['element' => 'span', 'content' => '$entity.public_notes', 'properties' => ['data-element' => 'total-table-public-notes-label']],
]], ]],
]; ];

View File

@ -95,34 +95,51 @@ trait DesignHelpers
* Logic below will help us calculate that & inject the result in the * Logic below will help us calculate that & inject the result in the
* global state of the $context (design state). * global state of the $context (design state).
* *
* @param string $type "product" or "task"
* @return void * @return void
*/ */
public function processTaxColumns(): void public function processTaxColumns(string $type): void
{ {
if (in_array('$product.tax', (array) $this->context['pdf_variables']['product_columns'])) { if ($type == 'product') {
$line_items = collect($this->entity->line_items); $type_id = 1;
}
if ($type == 'task') {
$type_id = 2;
}
// At the moment we pass "task" or "product" as type.
// However, "pdf_variables" contains "$task.tax" or "$product.tax" <-- Notice the dollar sign.
// This sprintf() will help us convert "task" or "product" into "$task" or "$product" without
// evaluating the variable.
if (in_array(sprintf('%s%s.tax', '$', $type), (array) $this->context['pdf_variables']["{$type}_columns"])) {
$line_items = collect($this->entity->line_items)->filter(function ($item) use ($type_id) {
return $item->type_id = $type_id;
});
$tax1 = $line_items->where('tax_name1', '<>', '')->where('type_id', $type_id)->count();
$tax2 = $line_items->where('tax_name2', '<>', '')->where('type_id', $type_id)->count();
$tax3 = $line_items->where('tax_name3', '<>', '')->where('type_id', $type_id)->count();
$tax1 = $line_items->where('tax_name1', '<>', '')->where('type_id', 1)->count();
$tax2 = $line_items->where('tax_name2', '<>', '')->where('type_id', 1)->count();
$tax3 = $line_items->where('tax_name3', '<>', '')->where('type_id', 1)->count();
$taxes = []; $taxes = [];
if ($tax1 > 0) { if ($tax1 > 0) {
array_push($taxes, '$product.tax_rate1'); array_push($taxes, sprintf('%s%s.tax_rate1', '$', $type));
} }
if ($tax2 > 0) { if ($tax2 > 0) {
array_push($taxes, '$product.tax_rate2'); array_push($taxes, sprintf('%s%s.tax_rate2', '$', $type));
} }
if ($tax3 > 0) { if ($tax3 > 0) {
array_push($taxes, '$product.tax_rate3'); array_push($taxes, sprintf('%s%s.tax_rate3', '$', $type));
} }
$key = array_search('$product.tax', $this->context['pdf_variables']['product_columns'], true); $key = array_search(sprintf('%s%s.tax', '$', $type), $this->context['pdf_variables']["{$type}_columns"], true);
if ($key) { if ($key) {
array_splice($this->context['pdf_variables']['product_columns'], $key, 1, $taxes); array_splice($this->context['pdf_variables']["{$type}_columns"], $key, 1, $taxes);
} }
} }
} }

View File

@ -304,8 +304,8 @@ class HtmlEngine
$data['$task.discount'] = ['value' => '', 'label' => ctrans('texts.discount')]; $data['$task.discount'] = ['value' => '', 'label' => ctrans('texts.discount')];
$data['$task.product_key'] = ['value' => '', 'label' => ctrans('texts.product_key')]; $data['$task.product_key'] = ['value' => '', 'label' => ctrans('texts.product_key')];
$data['$task.notes'] = ['value' => '', 'label' => ctrans('texts.notes')]; $data['$task.notes'] = ['value' => '', 'label' => ctrans('texts.notes')];
$data['$task.cost'] = ['value' => '', 'label' => ctrans('texts.cost')]; $data['$task.rate'] = ['value' => '', 'label' => ctrans('texts.rate')];
$data['$task.quantity'] = ['value' => '', 'label' => ctrans('texts.quantity')]; $data['$task.hours'] = ['value' => '', 'label' => ctrans('texts.hours')];
$data['$task.tax'] = ['value' => '', 'label' => ctrans('texts.tax')]; $data['$task.tax'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$task.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')]; $data['$task.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$task.tax_name2'] = ['value' => '', 'label' => ctrans('texts.tax')]; $data['$task.tax_name2'] = ['value' => '', 'label' => ctrans('texts.tax')];

View File

@ -117,7 +117,7 @@ class Phantom
'client' => $this->entity->client, 'client' => $this->entity->client,
'entity' => $this->entity, 'entity' => $this->entity,
'pdf_variables' => (array) $this->entity->company->settings->pdf_variables, 'pdf_variables' => (array) $this->entity->company->settings->pdf_variables,
'products' => $design->design->product, '$product' => $design->design->product,
]), ]),
'variables' => $html->generateLabelsAndValues(), 'variables' => $html->generateLabelsAndValues(),
'options' => [ 'options' => [

View File

@ -193,8 +193,8 @@ trait MakesTemplateData
$data['$task.discount'] = ['value' => '5%', 'label' => ctrans('texts.discount')]; $data['$task.discount'] = ['value' => '5%', 'label' => ctrans('texts.discount')];
$data['$task.product_key'] = ['value' => 'key', 'label' => ctrans('texts.product_key')]; $data['$task.product_key'] = ['value' => 'key', 'label' => ctrans('texts.product_key')];
$data['$task.notes'] = ['value' => 'Note for Tasks', 'label' => ctrans('texts.notes')]; $data['$task.notes'] = ['value' => 'Note for Tasks', 'label' => ctrans('texts.notes')];
$data['$task.cost'] = ['value' => '$100.00', 'label' => ctrans('texts.cost')]; $data['$task.rate'] = ['value' => '$100.00', 'label' => ctrans('texts.rate')];
$data['$task.quantity'] = ['value' => '1', 'label' => ctrans('texts.quantity')]; $data['$task.hours'] = ['value' => '1', 'label' => ctrans('texts.hours')];
$data['$task.tax_name1'] = ['value' => 'GST', 'label' => ctrans('texts.tax')]; $data['$task.tax_name1'] = ['value' => 'GST', 'label' => ctrans('texts.tax')];
$data['$task.tax_name2'] = ['value' => 'VAT', 'label' => ctrans('texts.tax')]; $data['$task.tax_name2'] = ['value' => 'VAT', 'label' => ctrans('texts.tax')];
$data['$task.tax_name3'] = ['value' => 'CA Sales Tax', 'label' => ctrans('texts.tax')]; $data['$task.tax_name3'] = ['value' => 'CA Sales Tax', 'label' => ctrans('texts.tax')];

View File

@ -58,7 +58,7 @@
flex-direction: column; flex-direction: column;
} }
#product-table { #product-table, #task-table {
min-width: 100%; min-width: 100%;
table-layout: fixed; table-layout: fixed;
overflow-wrap: break-word; overflow-wrap: break-word;
@ -66,33 +66,37 @@
margin-bottom: 200px; margin-bottom: 200px;
} }
#product-table > thead { #product-table, #task-table > thead {
text-align: left; text-align: left;
} }
#product-table > thead > tr > th { #product-table > thead > tr > th,
#task-table > thead > tr > th {
padding: 1rem; padding: 1rem;
background-color: #e6e6e6; background-color: #e6e6e6;
} }
#product-table > thead > tr > th:last-child { #product-table > thead > tr > th:last-child,
#task-table > thead > tr > th:last-child {
text-align: right; text-align: right;
} }
#product-table > tbody > tr > td { #product-table > tbody > tr > td,
#task-table > tbody > tr > td {
border-bottom: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6;
padding: 1rem; padding: 1rem;
} }
#product-table > tbody > tr > td:last-child { #product-table > tbody > tr > td:last-child,
#task-table > tbody > tr > td:last-child {
text-align: right; text-align: right;
} }
#product-table-footer { #table-totals {
page-break-inside: avoid; page-break-inside: avoid;
} }
#product-table-footer > * { #table-totals > * {
display: grid; display: grid;
grid-template-columns: 3fr 1fr 1fr; grid-template-columns: 3fr 1fr 1fr;
padding-top: .5rem; padding-top: .5rem;
@ -100,16 +104,16 @@
gap: 20px; gap: 20px;
} }
#product-table-footer #table-totals
> * > *
[data-element='product-table-balance-due-label'], [data-element='total-table-balance-due-label'],
#product-table-footer #table-totals
> * > *
[data-element='product-table-balance-due'] { [data-element='total-table-balance-due'] {
font-weight: bold; font-weight: bold;
} }
#product-table-footer > * > :last-child { #table-totals > * > :last-child {
text-align: right; text-align: right;
padding-right: 1rem; padding-right: 1rem;
} }
@ -137,7 +141,9 @@
<table id="product-table" cellspacing="0"></table> <table id="product-table" cellspacing="0"></table>
<div id="product-table-footer" cellspacing="0"></div> <table id="task-table" cellspacing="0"></table>
<div id="table-totals" cellspacing="0"></div>
</div> </div>
<div id="footer"></div> <div id="footer"></div>

View File

@ -80,7 +80,7 @@ class HtmlGenerationTest extends TestCase
'client' => $entity->client, 'client' => $entity->client,
'entity' => $entity, 'entity' => $entity,
'pdf_variables' => (array) $entity->company->settings->pdf_variables, 'pdf_variables' => (array) $entity->company->settings->pdf_variables,
'products' => $design->design->product, '$product' => $design->design->product,
]), ]),
'variables' => $html->generateLabelsAndValues(), 'variables' => $html->generateLabelsAndValues(),
'options' => [ 'options' => [