• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

AJenbo / agcms / 20972217862

13 Jan 2026 08:53PM UTC coverage: 53.678% (+0.1%) from 53.541%
20972217862

Pull #74

github

web-flow
Merge 4fdfac7ee into 498ff829e
Pull Request #74: Add PHP versions 8.4 to 8.5 to CI matrix

248 of 345 new or added lines in 40 files covered. (71.88%)

6 existing lines in 5 files now uncovered.

2780 of 5179 relevant lines covered (53.68%)

13.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/application/inc/Services/InvoicePdfService.php
1
<?php
2

3
namespace App\Services;
4

5
use App\Countries;
6
use App\DTO\InvoiceItem;
7
use App\Enums\InvoiceStatus;
8
use App\Exceptions\InvalidInput;
9
use App\Models\Invoice;
10
use TCPDF;
11

12
class InvoicePdfService
13
{
14
    private const CELL_WIDTH_QUANTITY = 24;
15
    private const CELL_WIDTH_TITLE = 106;
16
    private const CELL_WIDTH_PRICE = 29;
17
    private const CELL_WIDTH_TOTAL = 34;
18
    private const MAX_PRODCUTS = 20;
19

20
    private TCPDF $pdf;
21
    private Invoice $invoice;
22

23
    /**
24
     * Create the service.
25
     *
26
     * @throws InvalidInput
27
     */
28
    public function __construct(Invoice $invoice)
29
    {
30
        if (InvoiceStatus::New === $invoice->getStatus()) {
×
31
            throw new InvalidInput(_('Can\'t print invoice before it\'s locked.'));
×
32
        }
33

34
        $this->invoice = $invoice;
×
35

36
        $this->pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
×
37
        $this->setupDocument();
×
38
        $this->generateHeader();
×
39
        $this->addProductTable();
×
40
        $this->generateFooter();
×
41
    }
42

43
    /**
44
     * Get the PDF as a blob.
45
     */
46
    public function getStream(): string
47
    {
48
        return $this->pdf->Output('', 'S');
×
49
    }
50

51
    /**
52
     * Set up document defaults, title, size and margins.
53
     */
54
    private function setupDocument(): void
55
    {
56
        // set document information
57
        $this->pdf->SetCreator(PDF_CREATOR);
×
58
        $this->pdf->SetAuthor(ConfigService::getString('site_name'));
×
59
        $this->pdf->SetTitle('Online faktura #' . $this->invoice->getId());
×
60

61
        // remove default header/footer
62
        $this->pdf->setPrintHeader(false);
×
63
        $this->pdf->setPrintFooter(false);
×
64

65
        // set default monospaced font
66
        $this->pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED);
×
67

68
        //set margins
69
        $this->pdf->SetMargins(8, 9, 8);
×
70

71
        //set auto page breaks
72
        $this->pdf->SetAutoPageBreak(true, PDF_MARGIN_BOTTOM);
×
73

74
        //set image scale factor
75
        $this->pdf->setImageScale(PDF_IMAGE_SCALE_RATIO);
×
76

77
        $this->pdf->setLanguageArray([
×
78
            'a_meta_language' => 'da',
79
            'w_page'          => 'side',
80
        ]);
81

82
        $this->pdf->AddPage();
×
83
    }
84

85
    /**
86
     * Generate the header part of the document.
87
     */
88
    private function generateHeader(): void
89
    {
90
        $this->insertPageTitle();
×
91
        $this->insertCompanyContacts();
×
92
        $this->addSeporationLines();
×
93
        $this->insertCustomerAddresses();
×
94
        $this->insertInvoiceInformation();
×
95
    }
96

97
    /**
98
     * Insert date, id and references.
99
     */
100
    private function insertInvoiceInformation(): void
101
    {
102
        $this->pdf->SetFont('times', '', 10);
×
103
        $this->pdf->SetMargins(8, 9, 8);
×
104
        $this->pdf->Write(0, "\n");
×
105
        $this->pdf->SetY(90.5);
×
106
        $info = '<strong>' . _('Date') . ':</strong> ' . date(_('m/d/Y'), $this->invoice->getTimeStamp());
×
107
        if ($this->invoice->getIref()) {
×
108
            $info .= '       <strong>' . _('Our ref.:') . '</strong> ' . $this->invoice->getIref();
×
109
        }
110
        if ($this->invoice->getEref()) {
×
111
            $info .= '       <strong>' . _('Their ref.:') . '</strong> ' . $this->invoice->getEref();
×
112
        }
113
        $this->pdf->writeHTML($info);
×
114

115
        $idText = '<strong>' . _('Online Invoice') . '</strong> ' . $this->invoice->getId();
×
116
        $this->pdf->SetFont('times', '', 26);
×
117
        $this->pdf->SetY(85);
×
118
        $this->pdf->writeHTML($idText, false, false, false, false, 'R');
×
119
    }
120

121
    /**
122
     * Add lines to seporate the document, client and company addresses.
123
     */
124
    private function addSeporationLines(): void
125
    {
126
        $this->pdf->SetLineWidth(0.5);
×
127
        //Horizontal
128
        $this->pdf->Line(8, 27, 150, 27);
×
129
        //Vertical
130
        $this->pdf->Line(152.5, 12, 152.5, 74.5);
×
131
    }
132

133
    /**
134
     * Insert the company name in big bold letters.
135
     */
136
    private function insertPageTitle(): void
137
    {
138
        $this->pdf->SetFont('times', 'B', 37.5);
×
139
        $this->pdf->Write(0, ConfigService::getString('site_name'));
×
140
    }
141

142
    /**
143
     * Insert company address, phone, email and bank account.
144
     */
145
    private function insertCompanyContacts(): void
146
    {
147
        $this->pdf->SetY(12);
×
148
        $this->pdf->SetFont('times', '', 10);
×
149
        $addressLine = ConfigService::getString('address') . "\n" . ConfigService::getString('postcode') . ' ' . ConfigService::getString('city') . "\n";
×
150
        $this->pdf->Write(0, $addressLine, '', false, 'R');
×
151
        $this->pdf->SetFont('times', 'B', 11);
×
152
        $this->pdf->Write(0, _('Phone:') . ' ' . ConfigService::getString('phone') . "\n", '', false, 'R');
×
153
        $this->pdf->SetFont('times', '', 10);
×
154

155
        if (!$this->invoice->getDepartment()) {
×
156
            $this->invoice->setDepartment(ConfigService::getDefaultEmail());
×
157
        }
158
        $domain = explode('/', ConfigService::getString('base_url'));
×
159
        $domain = $domain[count($domain) - 1];
×
160
        $this->pdf->Write(0, $this->invoice->getDepartment() . "\n" . $domain . "\n\n", '', false, 'R');
×
161
        $this->pdf->SetFont('times', '', 11);
×
162
        $this->pdf->Write(0, "Danske Bank (Giro)\nReg.: 9541 Kont.: 169 3336\n", '', false, 'R');
×
163
        $this->pdf->SetFont('times', '', 10);
×
164
        $this->pdf->Write(0, "\nIBAN: DK693 000 000-1693336\nSWIFT BIC: DABADKKK\n\n", '', false, 'R');
×
165
        $this->pdf->SetFont('times', 'B', 11);
×
166
        $this->pdf->Write(0, 'CVR 1308 1387', '', false, 'R');
×
167
    }
168

169
    /**
170
     * Insert billing and shipping addresses.
171
     */
172
    private function insertCustomerAddresses(): void
173
    {
174
        $countries = Countries::getOrdered();
×
175

176
        //Invoice address
177
        $address = $this->getBillingAddress($countries);
×
178
        $this->pdf->SetMargins(19, 0, 0);
×
179
        $this->pdf->Write(0, "\n");
×
180
        $this->pdf->SetY(35);
×
181
        $this->pdf->SetFont('times', '', 11);
×
182
        $this->pdf->Write(0, $address);
×
183

184
        //Delivery address
185
        $address = $this->getShippingAddress($countries);
×
186
        if ($address) {
×
187
            $this->pdf->SetMargins(110, 0, 0);
×
188
            $this->pdf->Write(0, "\n");
×
189
            $this->pdf->SetY(30.6);
×
190
            $this->pdf->SetFont('times', 'BI', 10);
×
191
            $this->pdf->Write(0, _('Delivery address:') . "\n");
×
192
            $this->pdf->SetFont('times', '', 11);
×
193
            $this->pdf->Write(0, $address);
×
194
        }
195
    }
196

197
    /**
198
     * Get the billing addres.
199
     *
200
     * @param string[] $countries
201
     */
202
    private function getBillingAddress(array $countries): string
203
    {
204
        $address = $this->invoice->getName();
×
205
        if ($this->invoice->getAttn()) {
×
206
            $address .= "\n" . _('Attn.:') . ' ' . $this->invoice->getAttn();
×
207
        }
208
        if ($this->invoice->getAddress()) {
×
209
            $address .= "\n" . $this->invoice->getAddress();
×
210
        }
211
        if ($this->invoice->getPostbox()) {
×
212
            $address .= "\n" . $this->invoice->getPostbox();
×
213
        }
214
        $cityLine = $this->invoice->getCity();
×
215
        if ($this->invoice->getPostcode()) {
×
216
            $cityLine = $this->invoice->getPostcode() . ' ' . $this->invoice->getCity();
×
217
        }
218
        $address .= "\n" . $cityLine;
×
219
        if ($this->invoice->getCountry() && 'DK' !== $this->invoice->getCountry()) {
×
220
            $address .= "\n" . $countries[$this->invoice->getCountry()];
×
221
        }
222

NEW
223
        return mb_trim($address);
×
224
    }
225

226
    /**
227
     * Get the shippig addres.
228
     *
229
     * @param string[] $countries
230
     */
231
    private function getShippingAddress(array $countries): string
232
    {
233
        if (!$this->invoice->hasShippingAddress()) {
×
234
            return '';
×
235
        }
236

237
        $address = $this->invoice->getShippingName();
×
238
        if ($this->invoice->getShippingAttn()) {
×
239
            $address .= "\n" . _('Attn.:') . ' ' . $this->invoice->getShippingAttn();
×
240
        }
241
        if ($this->invoice->getShippingAddress()) {
×
242
            $address .= "\n" . $this->invoice->getShippingAddress();
×
243
        }
244
        if ($this->invoice->getShippingAddress2()) {
×
245
            $address .= "\n" . $this->invoice->getShippingAddress2();
×
246
        }
247
        if ($this->invoice->getShippingPostbox()) {
×
248
            $address .= "\n" . $this->invoice->getShippingPostbox();
×
249
        }
250
        if ($this->invoice->getShippingPostcode()) {
×
251
            $address .= "\n" . $this->invoice->getShippingPostcode() . ' ' . $this->invoice->getShippingCity();
×
252
        } elseif ($this->invoice->getShippingCity()) {
×
253
            $address .= "\n" . $this->invoice->getShippingCity();
×
254
        }
255
        if ($this->invoice->getShippingCountry() && 'DK' !== $this->invoice->getShippingCountry()) {
×
256
            $address .= "\n" . $countries[$this->invoice->getShippingCountry()];
×
257
        }
258

NEW
259
        return mb_trim($address);
×
260
    }
261

262
    /**
263
     * Add product table.
264
     */
265
    private function addProductTable(): void
266
    {
267
        $this->pdf->SetY(85);
×
268
        $this->pdf->SetFont('times', '', 10);
×
269
        $this->pdf->SetLineWidth(0.2);
×
270
        $this->pdf->Cell(0, 10.5, '', 0, 1);
×
271

272
        //Header
273
        $this->pdf->Cell(self::CELL_WIDTH_QUANTITY, 5, _('Quantity'), 1, 0, 'L');
×
274
        $this->pdf->Cell(self::CELL_WIDTH_TITLE, 5, _('Title'), 1, 0, 'L');
×
275
        $this->pdf->Cell(self::CELL_WIDTH_PRICE, 5, _('unit price'), 1, 0, 'R');
×
276
        $this->pdf->Cell(self::CELL_WIDTH_TOTAL, 5, _('Total'), 1, 1, 'R');
×
277

278
        //Cells
279
        $productLines = 0;
×
280
        foreach ($this->invoice->getItems() as $item) {
×
281
            $productLines += $this->insertProductLine($item);
×
282
        }
283

284
        $this->insertTableSpacing(self::MAX_PRODCUTS - $productLines);
×
285
        $this->insertTableFooter();
×
286
    }
287

288
    /**
289
     * Insert a single product line in the product table.
290
     */
291
    private function insertProductLine(InvoiceItem $item): int
292
    {
293
        $value = $item->value * (1 + $this->invoice->getVat());
×
294
        $lineTotal = $value * $item->quantity;
×
NEW
295
        $decimalSeparator = valstring(localeconv()['mon_decimal_point'] ?? '.');
×
296

297
        $this->pdf->Cell(self::CELL_WIDTH_QUANTITY, 6, (string)$item->quantity, 'RL', 0, 'R');
×
298
        $lines = $this->pdf->MultiCell(self::CELL_WIDTH_TITLE, 6, $item->title, 'RL', 'L', false, 0);
×
NEW
299
        $this->pdf->Cell(self::CELL_WIDTH_PRICE, 6, number_format($value, 2, $decimalSeparator, ''), 'RL', 0, 'R');
×
NEW
300
        $this->pdf->Cell(self::CELL_WIDTH_TOTAL, 6, number_format($lineTotal, 2, $decimalSeparator, ''), 'RL', 1, 'R');
×
301

302
        if ($lines > 1) {
×
303
            $this->insertTableSpacing($lines - 1);
×
304
        }
305

306
        return $lines;
×
307
    }
308

309
    /**
310
     * Insert empty lines at the of the table to keep it at a consistent height.
311
     */
312
    private function insertTableSpacing(int $lines): void
313
    {
314
        $this->pdf->Cell(self::CELL_WIDTH_QUANTITY, 6 * $lines, '', 'RL', 0);
×
315
        $this->pdf->Cell(self::CELL_WIDTH_TITLE, 6 * $lines, '', 'RL', 0);
×
316
        $this->pdf->Cell(self::CELL_WIDTH_PRICE, 6 * $lines, '', 'RL', 0);
×
317
        $this->pdf->Cell(self::CELL_WIDTH_TOTAL, 6 * $lines, '', 'RL', 1);
×
318
    }
319

320
    /**
321
     * Set the table footer, contaning total amount, shipping, conditions.
322
     */
323
    private function insertTableFooter(): void
324
    {
NEW
325
        $decimalSeparator = valstring(localeconv()['mon_decimal_point'] ?? '.');
×
326

327
        $vatText = ($this->invoice->getVat() * 100) . _('% VAT is: ')
×
NEW
328
            . number_format($this->invoice->getNetAmount() * $this->invoice->getVat(), 2, $decimalSeparator, '');
×
NEW
329
        $shippingPrice = number_format($this->invoice->getShipping(), 2, $decimalSeparator, '');
×
UNCOV
330
        $finePrint = '<strong>' . _('Payment Terms:') . '</strong> ' . _('Initial net amount.')
×
331
            . '<small><br>'
332
            . _('In case of payment later than the stated deadline, 2% interest will be added per month.')
×
333
            . '</small>';
334

335
        $this->pdf->Cell(self::CELL_WIDTH_QUANTITY, 6, '', 'RL', 0);
×
336
        $this->pdf->Cell(self::CELL_WIDTH_TITLE, 6, $vatText, 'RL', 0);
×
337
        $this->pdf->Cell(self::CELL_WIDTH_PRICE, 6, _('Shipping'), 'RL', 0, 'R');
×
338
        $this->pdf->Cell(self::CELL_WIDTH_TOTAL, 6, $shippingPrice, 'RL', 1, 'R');
×
339

340
        $this->pdf->SetFont('times', '', 10);
×
341
        $cellWidth = self::CELL_WIDTH_QUANTITY + self::CELL_WIDTH_TITLE;
×
342
        $this->pdf->MultiCell($cellWidth, 9, $finePrint, 1, 'L', false, 0, null, null, false, 8, true, false);
×
343
        $this->pdf->SetFont('times', 'B', 11);
×
344
        $this->pdf->Cell(self::CELL_WIDTH_PRICE, 9, _('Total (USD)'), 1, 0, 'C');
×
345
        $this->pdf->SetFont('times', '', 11);
×
NEW
346
        $this->pdf->Cell(
×
347
            self::CELL_WIDTH_TOTAL,
348
            9,
NEW
349
            number_format($this->invoice->getAmount(), 2, $decimalSeparator, ''),
×
350
            1,
351
            1,
352
            'R'
353
        );
354
    }
355

356
    /**
357
     * Generate the footer part of the invoice.
358
     */
359
    private function generateFooter(): void
360
    {
361
        //Note
362
        $note = $this->getPaymentNote();
×
363
        $note .= $this->invoice->getNote();
×
NEW
364
        $note = mb_trim($note);
×
365

366
        if ($note) {
×
367
            $this->pdf->SetFont('times', 'B', 10);
×
368
            $this->pdf->Write(0, "\n" . _('Note:') . "\n");
×
369
            $this->pdf->SetFont('times', '', 10);
×
370
            $this->pdf->Write(0, $note);
×
371
        }
372

373
        $this->pdf->SetFont('times', 'B', 12);
×
374
        $this->pdf->SetMargins(137, 0, 0);
×
375
        $this->pdf->Write(0, "\n");
×
376
        $this->pdf->SetY(-52);
×
377
        $this->pdf->Write(0, _('Sincerely,') . "\n\n\n" . $this->invoice->getClerk() . "\n" . ConfigService::getString('site_name'));
×
378
    }
379

380
    /**
381
     * Generate the payment note containing date and type of payment.
382
     */
383
    private function getPaymentNote(): string
384
    {
385
        switch ($this->invoice->getStatus()) {
×
386
            case InvoiceStatus::Accepted:
×
387
                $note = _('Paid online');
×
388
                break;
×
389
            case InvoiceStatus::Giro:
×
390
                $note = _('Paid via giro');
×
391
                break;
×
392
            case InvoiceStatus::Cash:
×
393
                $note = _('Paid in cash');
×
394
                break;
×
395
            default:
396
                return '';
×
397
        }
398

399
        if (null === $this->invoice->getTimeStampPay()) {
×
400
            return $note . "\n";
×
401
        }
402

403
        return $note . ' d. ' . date(_('m/d/Y'), $this->invoice->getTimeStampPay()) . "\n";
×
404
    }
405
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc