mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-08-04 15:12:12 +00:00
* fix(pdf): put the page margin back on the report PDFs
The report templates carry no inset of their own. They set
`.sub-container { padding: 0px 20px }` and nothing else, and relied entirely on
dompdf's built-in 1.2cm page margin. #727 made DompdfDriver always inject an
`@page` rule from PdfPageSetup, and #735 defaulted those margins to zero so
invoice2 and estimate2 could bleed their header band to the paper edge. The
document templates were fine, they carry their own 30px/50px insets. The reports
were not: every one of them now renders flush against the paper, with the
company name's glyph box actually clipped 1.2pt above the top edge.
The zero default has to stay for documents, so reports get a margin of their
own: `pdf.page.report_margin`, PDF_REPORT_MARGIN, defaulting to the 1.2cm they
were drawn against. It is a separate key on purpose, so an operator tuning the
document margins for their invoice template does not silently reflow every
report as a side effect.
A page margin rather than padding on the templates because reports run to
several pages and padding only insets the first one. Sales by customer already
spans two on the demo data, and page two moves with the rest.
Plumbed as an optional PdfPageSetup on the driver contract, defaulting to the
configured page, so every existing call site renders exactly as before and only
the five report controllers ask for anything different.
Measured on the five reports, page 595.28 x 841.89pt, 1.2cm = 34.02pt: first
page ink moves from xMin 15.0-15.8 / yMin -1.2 to xMin 49.0-49.8 / yMin 32.8,
every axis shifting by exactly the margin.
Also fixes three untranslated keys this exposed: the expenses report printed its
column headings as the literal strings "expenses.date", "expenses.note" and
"expenses.amount", which have never existed in lang/en.json. They are now
pdf_expense_{date,note,amount}_label, and a test pins that every translation key
a report template uses resolves in English.
* fix(pdf): put the minus sign in front of the currency symbol
format_money_pdf() formatted the signed value and then concatenated the symbol,
so a negative amount came out as "$-24,738.00". Credit notes made that common:
every line on a credit note PDF reads negative, and one credit note in a period
is enough to make the customer sales report show a negative total.
The magnitude is formatted first now and a single minus is prefixed to the whole
assembled string, so the sign leads and the symbol stays glued to the digits.
Only the symbol-first branch changes. number_format() already put the sign in
front of the digits, so a trailing-symbol currency read "-24,738.00$" before and
is byte-identical after.
The sign is decided on the formatted digits rather than on the raw input, so an
amount that rounds away at the currency's precision renders as zero rather than
as "-0". A stray cent on a zero-precision currency is the case that needs it.
* refactor(pdf): one shared chrome for the report PDFs
The five report templates were five drifted copies of one 2018 stylesheet, and
the insets had stopped agreeing with each other. profit-loss alone put its
header and income row at +20px, its "Expenses" heading at +23px, its category
rows at +30px, and its total rule and NET PROFIT band at +0, because that markup
sat outside the container everything above it was in. Four left edges on one
page. Every report also carried the same self-cancelling total rule, where
`padding: 0px` follows the two longhands it silently overrides, and expenses
carried six rule blocks nothing referenced at all, including the only horizontal
rule in the file.
There is now one layout partial and one stylesheet, and each report is content
only: 236 lines down to 47 for profit-loss, and about 1200 lines deleted across
the five. One content edge, measured: every band starts at 34.016pt and every
amount ends at 561.260pt, on every page of every report.
What changed on the page:
- Real tables with a thead, so column headings repeat across page breaks. Only
expenses had headings before and none of them used thead. sales-items emitted
a separate table per item, which is why its rows never lined up.
- The company logo in the header, the same fallback-to-name pattern the document
templates use.
- An empty period renders a "no records" row. profit-loss, sales-items and
tax-summary rendered their total row and rule unconditionally, so a month with
no data showed a heading, a gap, a rule and a lone $0.00.
- Sections stay whole across a page break where they fit, and a section heading
never sits at the foot of a page with its rows overleaf.
- Credit notes stay in the sales totals, since a reversal netting the sale out is
correct, but the line is tagged so a CN- number is not read as a sale. It
reuses the document's own label, which is already in the shipped locales.
- Labels stopped carrying their own presentation: "TOTAL EXPENSE" (also
singular) is "Total expenses" and the stylesheet does the uppercasing.
The five controllers drop the dead colour-settings block: nine *_color settings
were queried and shared by every report, no template ever read them, and no
migration, seeder or UI ever wrote them, so the query always returned an empty
collection. Every other shared variable name is untouched, because a custom
report template is a copy that references them by name.
make:template had to learn the same lesson: it only ever copied
partials/table.blade.php, so a cloned report would extend a layout that does not
exist in its namespace and die on render. It now copies every partial a type
ships and rewrites references by view name, including partial-to-partial ones,
so each custom template still gets its own copies.
171 lines
6.1 KiB
PHP
171 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Support\Pdf\PdfTemplateUtils;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
class CreateTemplateCommand extends Command
|
|
{
|
|
/**
|
|
* Types you pick a design for. A custom template is a new, separately
|
|
* selectable entry in the picker, so the name is yours to choose and the
|
|
* clone source is the first built-in.
|
|
*
|
|
* The --type option is checked against these rather than only the
|
|
* interactive prompt: passing an unsupported one used to skip the prompt and
|
|
* die on an uncaught FileNotFoundException further down, with a stack trace
|
|
* instead of a message.
|
|
*/
|
|
private const SELECTABLE_TYPES = ['invoice', 'estimate'];
|
|
|
|
/**
|
|
* Types with no picker. A custom template here replaces the built-in outright
|
|
* (see PdfTemplateUtils::resolveView), so the name is not free: it has to
|
|
* match the document you are overriding.
|
|
*/
|
|
private const OVERRIDE_TYPES = [
|
|
'payment' => ['payment'],
|
|
'reports' => ['expenses', 'profit-loss', 'sales-customers', 'sales-items', 'tax-summary'],
|
|
];
|
|
|
|
private static function types(): array
|
|
{
|
|
return array_merge(self::SELECTABLE_TYPES, array_keys(self::OVERRIDE_TYPES));
|
|
}
|
|
|
|
/**
|
|
* The name and signature of the console command.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $signature = 'make:template {name} {--type=}';
|
|
|
|
/**
|
|
* The console command description.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $description = 'Create estimate or invoice pdf template.';
|
|
|
|
/**
|
|
* Execute the console command.
|
|
*/
|
|
public function handle(): int
|
|
{
|
|
$templateName = $this->argument('name');
|
|
$templateType = $this->option('type');
|
|
|
|
if (! $templateType) {
|
|
$templateType = $this->choice('Create a template for?', self::types());
|
|
}
|
|
|
|
if (! in_array($templateType, self::types(), true)) {
|
|
$this->error(sprintf(
|
|
'Unsupported template type "%s". Supported types: %s.',
|
|
$templateType,
|
|
implode(', ', self::types())
|
|
));
|
|
|
|
return self::INVALID;
|
|
}
|
|
|
|
if (! preg_match('/^[A-Za-z0-9._-]+$/', $templateName)) {
|
|
$this->error('Template name may only contain letters, numbers, dots, dashes and underscores.');
|
|
|
|
return self::INVALID;
|
|
}
|
|
|
|
$isOverride = array_key_exists($templateType, self::OVERRIDE_TYPES);
|
|
|
|
if ($isOverride && ! in_array($templateName, self::OVERRIDE_TYPES[$templateType], true)) {
|
|
$this->error(sprintf(
|
|
'"%s" is not a %s document. An override replaces a specific one, so the name must be one of: %s.',
|
|
$templateName,
|
|
$templateType,
|
|
implode(', ', self::OVERRIDE_TYPES[$templateType])
|
|
));
|
|
|
|
return self::INVALID;
|
|
}
|
|
|
|
if (PdfTemplateUtils::customTemplateFileExists($templateType, sprintf('%s.blade.php', $templateName))) {
|
|
$this->info('Template with given name already exists.');
|
|
|
|
return self::INVALID;
|
|
}
|
|
|
|
// An override clones the document it replaces; a selectable template
|
|
// clones the first built-in design.
|
|
$sourceName = $isOverride ? $templateName : "{$templateType}1";
|
|
|
|
$source = Storage::disk('views')->get("/app/pdf/{$templateType}/{$sourceName}.blade.php");
|
|
|
|
// Point this template at its own copies of the partials its type ships
|
|
// before the blanket namespace rewrite below catches them. Previously
|
|
// every custom template of a type included the same
|
|
// partials/table.blade.php, which was written once and then reused, so
|
|
// editing the table for one custom template silently changed it for all
|
|
// of them.
|
|
$source = PdfTemplateUtils::rewriteViewReferences(
|
|
$source,
|
|
PdfTemplateUtils::partialViewMap($templateType, $templateName),
|
|
);
|
|
|
|
$source = Str::replace(
|
|
sprintf('app.pdf.%s', $templateType),
|
|
sprintf('pdf_templates::%s', $templateType),
|
|
$source,
|
|
);
|
|
|
|
if (! PdfTemplateUtils::toCustomTemplateMarkupFile($source, $templateType, $templateName)) {
|
|
$this->error(sprintf('Unable to create %s template.', ucfirst($templateType)));
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
// Only selectable types need a preview: an override replaces one
|
|
// document outright and never appears in a picker.
|
|
if (! $isOverride) {
|
|
PdfTemplateUtils::toCustomTemplateImageFile(
|
|
File::get(resource_path("static/img/PDF/{$templateType}1.png")),
|
|
$templateType,
|
|
$templateName,
|
|
);
|
|
}
|
|
|
|
// Every partial of the type, not just the table: reports extend a shared
|
|
// layout which in turn includes a shared stylesheet, and a clone that
|
|
// was rewritten to the custom namespace without them pointed at views
|
|
// that did not exist and failed to render.
|
|
PdfTemplateUtils::copyTemplatePartials($templateType, $templateName);
|
|
|
|
// Repeating page header/footer, if the source template has one. Named
|
|
// with the {template}_header / {template}_footer suffix the Gotenberg
|
|
// driver looks for.
|
|
foreach (['_header', '_footer'] as $suffix) {
|
|
$companion = "/app/pdf/{$templateType}/{$sourceName}{$suffix}.blade.php";
|
|
|
|
if (Storage::disk('views')->exists($companion)) {
|
|
PdfTemplateUtils::toCustomTemplateFile(
|
|
Storage::disk('views')->get($companion),
|
|
$templateType,
|
|
sprintf('%s%s.blade.php', $templateName, $suffix),
|
|
);
|
|
}
|
|
}
|
|
|
|
$this->info(
|
|
sprintf('%s Template created successfully at %s',
|
|
ucfirst($templateType),
|
|
PdfTemplateUtils::getCustomTemplateFilePath($templateType, sprintf('%s.blade.php', $templateName))
|
|
)
|
|
);
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|