@design.estate/dees-document 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_bundle/bundle.js +594 -339
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.pdfservice.d.ts +3 -0
- package/dist_ts/classes.pdfservice.js +5 -1
- package/dist_ts_shared/index.d.ts +3 -0
- package/dist_ts_shared/index.js +4 -1
- package/dist_ts_shared/paidamount.d.ts +42 -0
- package/dist_ts_shared/paidamount.js +141 -0
- package/dist_ts_shared/paymentreminder.d.ts +28 -0
- package/dist_ts_shared/paymentreminder.js +33 -0
- package/dist_ts_shared/translation.d.ts +51 -0
- package/dist_ts_shared/translation.js +106 -1
- package/dist_ts_shared/unitnames.d.ts +44 -0
- package/dist_ts_shared/unitnames.js +103 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/contentinvoice.d.ts +101 -1
- package/dist_ts_web/elements/contentinvoice.js +654 -44
- package/dist_ts_web/elements/document.js +13 -1
- package/dist_ts_web/elements/letterheader.d.ts +2 -0
- package/dist_ts_web/elements/letterheader.js +20 -1
- package/package.json +2 -2
- package/readme.md +117 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.pdfservice.ts +4 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/contentinvoice.ts +802 -49
- package/ts_web/elements/document.ts +11 -0
- package/ts_web/elements/letterheader.ts +28 -0
|
@@ -61,6 +61,14 @@ const accountingDocContents: Record<
|
|
|
61
61
|
requestsPayment: true,
|
|
62
62
|
printsRelatedDocuments: false,
|
|
63
63
|
},
|
|
64
|
+
// a complete invoice in place of one that lacked a detail or stated one
|
|
65
|
+
// incorrectly (§ 31 Abs. 5 UStDV); it names that invoice in its own block,
|
|
66
|
+
// renderCorrection(), not among the related documents
|
|
67
|
+
"corrected-invoice": {
|
|
68
|
+
introStatement: "invoice@@introStatement",
|
|
69
|
+
requestsPayment: true,
|
|
70
|
+
printsRelatedDocuments: false,
|
|
71
|
+
},
|
|
64
72
|
// reduces what the recipient owes: nothing to pay, and it names the document it corrects
|
|
65
73
|
creditnote: {
|
|
66
74
|
introStatement: "creditnote@@introStatement",
|
|
@@ -97,6 +105,160 @@ const relatedDocumentLabels: Record<
|
|
|
97
105
|
references: "relatedDocument.references",
|
|
98
106
|
};
|
|
99
107
|
|
|
108
|
+
/** A column that takes the width of its widest cell. */
|
|
109
|
+
interface IFittedColumn {
|
|
110
|
+
/** the cells of the column, the header included */
|
|
111
|
+
cellSelector: string;
|
|
112
|
+
/** the custom property the grid reads the column's width from */
|
|
113
|
+
widthProperty: string;
|
|
114
|
+
/** the width the column never goes below, in px */
|
|
115
|
+
minWidth: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The fitted columns of a table and the room the rest of a row takes. */
|
|
119
|
+
interface IColumnLayout {
|
|
120
|
+
columns: IFittedColumn[];
|
|
121
|
+
/** the columns of fixed width, together, in px */
|
|
122
|
+
fixedWidth: number;
|
|
123
|
+
/** the least room the flexible column (the description) keeps, in px */
|
|
124
|
+
flexibleMinWidth: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The positions: number 40 px and VAT rate 50 px fixed, the description
|
|
129
|
+
* flexible with at least 120 px (as the grid in the styles declares); the
|
|
130
|
+
* last column is the one the sums line up with.
|
|
131
|
+
*/
|
|
132
|
+
const positionColumns: IColumnLayout = {
|
|
133
|
+
columns: [
|
|
134
|
+
{ cellSelector: ".quantityCell", widthProperty: "--quantity-column-width", minWidth: 50 },
|
|
135
|
+
{ cellSelector: ".unitCell", widthProperty: "--unit-column-width", minWidth: 50 },
|
|
136
|
+
{ cellSelector: ".unitPriceCell", widthProperty: "--unit-price-column-width", minWidth: 100 },
|
|
137
|
+
{ cellSelector: ".amountCell", widthProperty: "--amount-column-width", minWidth: 100 },
|
|
138
|
+
],
|
|
139
|
+
fixedWidth: 90,
|
|
140
|
+
flexibleMinWidth: 120,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A reminder's claims, and its charges and sums, which share the last column;
|
|
145
|
+
* the document label is flexible with at least 150 px (as the grid in the
|
|
146
|
+
* styles declares).
|
|
147
|
+
*/
|
|
148
|
+
const reminderColumns: IColumnLayout = {
|
|
149
|
+
columns: [
|
|
150
|
+
{ cellSelector: ".issueDateCell", widthProperty: "--issue-date-column-width", minWidth: 80 },
|
|
151
|
+
{ cellSelector: ".dueDateCell", widthProperty: "--due-date-column-width", minWidth: 80 },
|
|
152
|
+
{ cellSelector: ".grossCell", widthProperty: "--gross-column-width", minWidth: 100 },
|
|
153
|
+
{ cellSelector: ".amountCell", widthProperty: "--amount-column-width", minWidth: 100 },
|
|
154
|
+
],
|
|
155
|
+
fixedWidth: 0,
|
|
156
|
+
flexibleMinWidth: 150,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** The smallest font a cell is set in to fit its column, in px. */
|
|
160
|
+
const MIN_FITTED_FONT_SIZE = 8;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The width a cell's text needs, its padding included, measured on the text
|
|
164
|
+
* itself, so it is the same whether the text fits its column or overflows it.
|
|
165
|
+
*/
|
|
166
|
+
const neededWidth = (cellArg: HTMLElement): number => {
|
|
167
|
+
const range = document.createRange();
|
|
168
|
+
range.selectNodeContents(cellArg);
|
|
169
|
+
const style = getComputedStyle(cellArg);
|
|
170
|
+
return Math.ceil(
|
|
171
|
+
range.getBoundingClientRect().width +
|
|
172
|
+
parseFloat(style.paddingLeft) +
|
|
173
|
+
parseFloat(style.paddingRight)
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Widths for columns that want `wantedArg` and get at least `minimumsArg`,
|
|
179
|
+
* within `availableArg`: what is left above the minimums is granted in equal
|
|
180
|
+
* shares, a column never getting more than it wants.
|
|
181
|
+
*/
|
|
182
|
+
const shareWidth = (
|
|
183
|
+
wantedArg: number[],
|
|
184
|
+
minimumsArg: number[],
|
|
185
|
+
availableArg: number
|
|
186
|
+
): number[] => {
|
|
187
|
+
const widths = [...minimumsArg];
|
|
188
|
+
let remaining =
|
|
189
|
+
availableArg - minimumsArg.reduce((sumArg, widthArg) => sumArg + widthArg, 0);
|
|
190
|
+
let open = widths.map((_, index) => index).filter((index) => wantedArg[index] > widths[index]);
|
|
191
|
+
while (open.length > 0 && remaining >= 1) {
|
|
192
|
+
const share = remaining / open.length;
|
|
193
|
+
for (const index of open) {
|
|
194
|
+
const grant = Math.min(share, wantedArg[index] - widths[index]);
|
|
195
|
+
widths[index] += grant;
|
|
196
|
+
remaining -= grant;
|
|
197
|
+
}
|
|
198
|
+
open = open.filter((index) => wantedArg[index] - widths[index] >= 1);
|
|
199
|
+
}
|
|
200
|
+
return widths.map((widthArg) => Math.floor(widthArg));
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Sets a cell whose text is wider than its column in a smaller font, down to
|
|
205
|
+
* `MIN_FITTED_FONT_SIZE`, and lets it wrap within the column when even that
|
|
206
|
+
* is too wide.
|
|
207
|
+
*/
|
|
208
|
+
const fitCellText = (cellArg: HTMLElement, columnWidthArg: number): void => {
|
|
209
|
+
const needed = neededWidth(cellArg);
|
|
210
|
+
if (needed <= columnWidthArg) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const style = getComputedStyle(cellArg);
|
|
214
|
+
const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
|
215
|
+
const fontSize =
|
|
216
|
+
(parseFloat(style.fontSize) * (columnWidthArg - padding)) / (needed - padding);
|
|
217
|
+
if (fontSize >= MIN_FITTED_FONT_SIZE) {
|
|
218
|
+
cellArg.style.fontSize = `${Math.floor(fontSize * 10) / 10}px`;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
cellArg.style.fontSize = `${MIN_FITTED_FONT_SIZE}px`;
|
|
222
|
+
cellArg.style.whiteSpace = "normal";
|
|
223
|
+
cellArg.style.overflowWrap = "anywhere";
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/** The kind of a reminded document, as the claims table names it. */
|
|
227
|
+
const claimDocumentLabels: Record<
|
|
228
|
+
plugins.tsclass.finance.TPaymentReminderClaim["documentType"],
|
|
229
|
+
plugins.shared.translation.TranslationKey
|
|
230
|
+
> = {
|
|
231
|
+
invoice: "paymentReminder@@claim.invoice",
|
|
232
|
+
"corrected-invoice": "paymentReminder@@claim.correctedInvoice",
|
|
233
|
+
debitnote: "paymentReminder@@claim.debitnote",
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** The words for a detail of § 14 Abs. 4 Satz 1 UStG a corrected invoice supplies or corrects. */
|
|
237
|
+
const correctionDetailLabels: Record<
|
|
238
|
+
plugins.tsclass.finance.TInvoiceMandatoryDetail,
|
|
239
|
+
plugins.shared.translation.TranslationKey
|
|
240
|
+
> = {
|
|
241
|
+
parties: "correctedInvoice.detail.parties",
|
|
242
|
+
"supplier-tax-number": "correctedInvoice.detail.supplierTaxNumber",
|
|
243
|
+
"issue-date": "correctedInvoice.detail.issueDate",
|
|
244
|
+
"invoice-number": "correctedInvoice.detail.invoiceNumber",
|
|
245
|
+
"supply-description": "correctedInvoice.detail.supplyDescription",
|
|
246
|
+
"supply-date": "correctedInvoice.detail.supplyDate",
|
|
247
|
+
"net-amount": "correctedInvoice.detail.netAmount",
|
|
248
|
+
tax: "correctedInvoice.detail.tax",
|
|
249
|
+
"retention-note": "correctedInvoice.detail.retentionNote",
|
|
250
|
+
"special-case-detail": "correctedInvoice.detail.specialCaseDetail",
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/** What was done with a detail: supplied when it was missing, corrected when it was incorrect. */
|
|
254
|
+
const correctionDefectLabels: Record<
|
|
255
|
+
plugins.tsclass.finance.TInvoiceCorrection["defect"],
|
|
256
|
+
plugins.shared.translation.TranslationKey
|
|
257
|
+
> = {
|
|
258
|
+
missing: "correctedInvoice.defect.missing",
|
|
259
|
+
incorrect: "correctedInvoice.defect.incorrect",
|
|
260
|
+
};
|
|
261
|
+
|
|
100
262
|
@customElement("dedocument-contentinvoice")
|
|
101
263
|
export class DeContentInvoice extends DeesElement {
|
|
102
264
|
public static demo = () => html`
|
|
@@ -135,6 +297,18 @@ export class DeContentInvoice extends DeesElement {
|
|
|
135
297
|
: null;
|
|
136
298
|
}
|
|
137
299
|
|
|
300
|
+
/** The payment reminder this content prints, or null. */
|
|
301
|
+
private get paymentReminder(): plugins.tsclass.finance.TPaymentReminder | null {
|
|
302
|
+
return plugins.shared.isPaymentReminder(this.letterData)
|
|
303
|
+
? this.letterData
|
|
304
|
+
: null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The currency of the letter's amounts: an accounting document's or a reminder's. */
|
|
308
|
+
private get currency(): string | undefined {
|
|
309
|
+
return this.accountingDoc?.currency ?? this.paymentReminder?.currency;
|
|
310
|
+
}
|
|
311
|
+
|
|
138
312
|
/**
|
|
139
313
|
* The document builds this element for every letter; one that is no
|
|
140
314
|
* accounting document, and one of a type this version does not know, keep
|
|
@@ -174,7 +348,7 @@ export class DeContentInvoice extends DeesElement {
|
|
|
174
348
|
|
|
175
349
|
.grid {
|
|
176
350
|
display: grid;
|
|
177
|
-
grid-template-columns: 40px
|
|
351
|
+
grid-template-columns: 40px minmax(120px, 1fr) var(--quantity-column-width, 50px) var(--unit-column-width, 50px) var(--unit-price-column-width, 100px) 50px var(--amount-column-width, 100px);
|
|
178
352
|
}
|
|
179
353
|
|
|
180
354
|
.topLine {
|
|
@@ -194,6 +368,16 @@ export class DeContentInvoice extends DeesElement {
|
|
|
194
368
|
border-right: none;
|
|
195
369
|
}
|
|
196
370
|
|
|
371
|
+
/*
|
|
372
|
+
* the description takes the room the other columns leave, at least its
|
|
373
|
+
* minimum, and wraps within it, so a long one never widens the table
|
|
374
|
+
* beyond the page
|
|
375
|
+
*/
|
|
376
|
+
.lineItem.descriptionCell {
|
|
377
|
+
white-space: normal;
|
|
378
|
+
overflow-wrap: anywhere;
|
|
379
|
+
}
|
|
380
|
+
|
|
197
381
|
.value.rightAlign,
|
|
198
382
|
.lineItem.rightAlign {
|
|
199
383
|
text-align: right;
|
|
@@ -211,6 +395,36 @@ export class DeContentInvoice extends DeesElement {
|
|
|
211
395
|
box-sizing: content-box;
|
|
212
396
|
}
|
|
213
397
|
|
|
398
|
+
.claimGrid {
|
|
399
|
+
display: grid;
|
|
400
|
+
grid-template-columns: minmax(150px, 1fr) var(--issue-date-column-width, 80px) var(--due-date-column-width, 80px) var(--gross-column-width, 100px) var(--amount-column-width, 100px);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.chargeGrid {
|
|
404
|
+
display: grid;
|
|
405
|
+
grid-template-columns: auto var(--amount-column-width, 100px);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.chargeGrid.dataHeader {
|
|
409
|
+
margin-top: 12px;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* a charge's description and basis wrap: the basis of default interest is longer than a line */
|
|
413
|
+
.lineItem.chargeText {
|
|
414
|
+
white-space: normal;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.chargeBasis {
|
|
418
|
+
margin-top: 2px;
|
|
419
|
+
font-size: 11px;
|
|
420
|
+
color: #555;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* a provision is not split across lines */
|
|
424
|
+
.citation {
|
|
425
|
+
white-space: nowrap;
|
|
426
|
+
}
|
|
427
|
+
|
|
214
428
|
.sums {
|
|
215
429
|
margin-top: 5px;
|
|
216
430
|
font-size: 12px;
|
|
@@ -220,7 +434,7 @@ export class DeContentInvoice extends DeesElement {
|
|
|
220
434
|
.sums .sumline {
|
|
221
435
|
margin-top: 3px;
|
|
222
436
|
display: grid;
|
|
223
|
-
grid-template-columns: auto 100px;
|
|
437
|
+
grid-template-columns: auto var(--amount-column-width, 100px);
|
|
224
438
|
}
|
|
225
439
|
|
|
226
440
|
.sums .sumline .label {
|
|
@@ -238,6 +452,21 @@ export class DeContentInvoice extends DeesElement {
|
|
|
238
452
|
font-weight: bold;
|
|
239
453
|
}
|
|
240
454
|
|
|
455
|
+
/* the tax an advance payment contains is stated, not added up */
|
|
456
|
+
.sums .sumline--contained .label,
|
|
457
|
+
.sums .sumline--contained .value {
|
|
458
|
+
font-weight: normal;
|
|
459
|
+
font-size: 11px;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.advancePayment {
|
|
463
|
+
margin-top: 2px;
|
|
464
|
+
padding-left: 20%;
|
|
465
|
+
font-size: 11px;
|
|
466
|
+
text-align: right;
|
|
467
|
+
color: #555;
|
|
468
|
+
}
|
|
469
|
+
|
|
241
470
|
.divider {
|
|
242
471
|
margin-top: 8px;
|
|
243
472
|
border-top: 1px dotted #ccc;
|
|
@@ -264,6 +493,19 @@ export class DeContentInvoice extends DeesElement {
|
|
|
264
493
|
margin-top: 4px;
|
|
265
494
|
}
|
|
266
495
|
|
|
496
|
+
.correction {
|
|
497
|
+
margin-bottom: 12px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.correctionTitle {
|
|
501
|
+
font-weight: bold;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.correctionDetails ul {
|
|
505
|
+
margin: 2px 0 0 0;
|
|
506
|
+
padding-left: 18px;
|
|
507
|
+
}
|
|
508
|
+
|
|
267
509
|
.relatedDocuments .label {
|
|
268
510
|
font-weight: bold;
|
|
269
511
|
}
|
|
@@ -294,7 +536,7 @@ export class DeContentInvoice extends DeesElement {
|
|
|
294
536
|
* an unknown or missing currency falls back to a plain decimal number.
|
|
295
537
|
*/
|
|
296
538
|
protected formatPrice(value: number): string {
|
|
297
|
-
const currency = this.
|
|
539
|
+
const currency = this.currency;
|
|
298
540
|
const locale = localeForLanguage(this.documentSettings?.languageCode);
|
|
299
541
|
if (currency) {
|
|
300
542
|
try {
|
|
@@ -306,6 +548,39 @@ export class DeContentInvoice extends DeesElement {
|
|
|
306
548
|
return new Intl.NumberFormat(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
|
|
307
549
|
}
|
|
308
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Formats a quantity in the document's language with every decimal it
|
|
553
|
+
* carries: 12.5 prints as "12,5" in German and "12.5" in English, and no
|
|
554
|
+
* stated quantity is rounded. The decimals are those of the shortest
|
|
555
|
+
* representation of the number, the one `String()` gives, as the unit name
|
|
556
|
+
* counts them for its singular or plural.
|
|
557
|
+
*/
|
|
558
|
+
protected formatQuantity(quantityArg: number): string {
|
|
559
|
+
const fractionDigits = plugins.shared.quantityFractionDigits(quantityArg);
|
|
560
|
+
return new Intl.NumberFormat(
|
|
561
|
+
localeForLanguage(this.documentSettings?.languageCode),
|
|
562
|
+
{
|
|
563
|
+
minimumFractionDigits: fractionDigits,
|
|
564
|
+
maximumFractionDigits: fractionDigits,
|
|
565
|
+
}
|
|
566
|
+
).format(quantityArg);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Formats a rate in percent, with as many decimals as it states, up to four. */
|
|
570
|
+
protected formatPercent(percentArg: number): string {
|
|
571
|
+
return new Intl.NumberFormat(
|
|
572
|
+
localeForLanguage(this.documentSettings?.languageCode),
|
|
573
|
+
{ style: "percent", maximumFractionDigits: 4 }
|
|
574
|
+
).format(percentArg / 100);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Formats a day in the document's language and date style. */
|
|
578
|
+
protected formatDay(timestampArg: number): string {
|
|
579
|
+
return new Intl.DateTimeFormat(this.documentSettings.languageCode, {
|
|
580
|
+
dateStyle: this.documentSettings.dateStyle,
|
|
581
|
+
}).format(new Date(timestampArg));
|
|
582
|
+
}
|
|
583
|
+
|
|
309
584
|
public getTotalNet = (): number => {
|
|
310
585
|
let totalNet = 0;
|
|
311
586
|
|
|
@@ -485,24 +760,33 @@ export class DeContentInvoice extends DeesElement {
|
|
|
485
760
|
}
|
|
486
761
|
}
|
|
487
762
|
|
|
488
|
-
/**
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
763
|
+
/**
|
|
764
|
+
* The due date, which only an accounting document and a payment reminder
|
|
765
|
+
* have: it counts `dueInDays` from the letter's date. A document that
|
|
766
|
+
* states a paid amount names the amount due, since the total is not what
|
|
767
|
+
* remains to be paid.
|
|
768
|
+
*/
|
|
769
|
+
private renderPaymentTerms(optionsArg: {
|
|
770
|
+
date: number;
|
|
771
|
+
dueInDays: number;
|
|
772
|
+
labelKey: plugins.shared.translation.TranslationKey;
|
|
773
|
+
textKey: plugins.shared.translation.TranslationKey;
|
|
774
|
+
/** the amount due, when the document states a paid amount */
|
|
775
|
+
amountDue?: number;
|
|
776
|
+
}): TemplateResult {
|
|
492
777
|
return html`<div class="infoBox">
|
|
493
778
|
<div>
|
|
494
779
|
<div>
|
|
495
|
-
<div class="label">
|
|
496
|
-
${this.translateKey("invoice@@payment.terms")}
|
|
497
|
-
</div>
|
|
780
|
+
<div class="label">${this.translateKey(optionsArg.labelKey)}</div>
|
|
498
781
|
<span>
|
|
499
|
-
${
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
782
|
+
${optionsArg.amountDue === undefined
|
|
783
|
+
? this.translateKey(optionsArg.textKey)
|
|
784
|
+
: html`${this.translateKey("payment.amountDue")}:
|
|
785
|
+
${this.formatPrice(optionsArg.amountDue)}
|
|
786
|
+
${this.translateKey("payment.terms.amountDue")}`}
|
|
787
|
+
${this.formatDay(
|
|
788
|
+
new Date(optionsArg.date).setDate(
|
|
789
|
+
new Date(optionsArg.date).getDate() + optionsArg.dueInDays
|
|
506
790
|
)
|
|
507
791
|
)}
|
|
508
792
|
</span>
|
|
@@ -516,15 +800,21 @@ export class DeContentInvoice extends DeesElement {
|
|
|
516
800
|
* code: a euro amount to the sender's IBAN, which a document in another
|
|
517
801
|
* currency or without the sender's IBAN does not have.
|
|
518
802
|
*/
|
|
519
|
-
private renderPaymentInfo(
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
803
|
+
private renderPaymentInfo(optionsArg: {
|
|
804
|
+
/** the sender, who is paid */
|
|
805
|
+
from: plugins.tsclass.business.TContact;
|
|
806
|
+
currency: string;
|
|
807
|
+
/** the amount to pay */
|
|
808
|
+
amount: number;
|
|
809
|
+
/** the number of the letter, the remittance information */
|
|
810
|
+
reference: string;
|
|
811
|
+
}): TemplateResult | null {
|
|
812
|
+
const bic = optionsArg.from.sepaConnection?.bic;
|
|
813
|
+
const name = optionsArg.from.name;
|
|
814
|
+
const iban = optionsArg.from.sepaConnection?.iban;
|
|
815
|
+
const currency = optionsArg.currency;
|
|
816
|
+
const totalGross = optionsArg.amount;
|
|
817
|
+
const reference = optionsArg.reference;
|
|
528
818
|
if (
|
|
529
819
|
typeof iban !== "string" ||
|
|
530
820
|
plugins.shared.buildEpcQrPayload({
|
|
@@ -557,6 +847,115 @@ export class DeContentInvoice extends DeesElement {
|
|
|
557
847
|
</div>`;
|
|
558
848
|
}
|
|
559
849
|
|
|
850
|
+
/**
|
|
851
|
+
* The paid amount and the amount due, below the gross total, when the
|
|
852
|
+
* document states a paid amount (EN 16931 BT-113 and BT-115). The advance
|
|
853
|
+
* payments a final invoice deducts are deducted as the gross total received,
|
|
854
|
+
* with the tax it contains stated per rate, as Abschnitt 14.8 Abs. 7 Satz 3
|
|
855
|
+
* UStAE allows: "Statt der vorausgezahlten Entgelte oder Teilentgelte und
|
|
856
|
+
* der Steuerbeträge können auch die Gesamtbeträge der Voraus- oder
|
|
857
|
+
* Anzahlungen abgesetzt und die darin enthaltenen Steuerbeträge zusätzlich
|
|
858
|
+
* angegeben werden." More paid than the total prints as a credit balance.
|
|
859
|
+
*/
|
|
860
|
+
private renderPaidAmountSums(
|
|
861
|
+
summaryArg: plugins.shared.IPaidAmountSummary
|
|
862
|
+
): TemplateResult {
|
|
863
|
+
return html`<div class="sumline">
|
|
864
|
+
<div class="label">${this.translateKey("payment.paid")}</div>
|
|
865
|
+
<div class="value rightAlign amountCell">
|
|
866
|
+
${this.formatPrice(summaryArg.paidAmount)}
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
869
|
+
${summaryArg.advancePaymentTaxes.map(
|
|
870
|
+
(taxArg) => html`<div class="sumline sumline--contained">
|
|
871
|
+
<div class="label">
|
|
872
|
+
${this.translateKey("advancePayment.vat")} ${taxArg.percentage}%
|
|
873
|
+
</div>
|
|
874
|
+
<div class="value rightAlign amountCell">
|
|
875
|
+
${this.formatPrice(taxArg.vat)}
|
|
876
|
+
</div>
|
|
877
|
+
</div>`
|
|
878
|
+
)}
|
|
879
|
+
<div class="sumline">
|
|
880
|
+
<div class="label">
|
|
881
|
+
${this.translateKey(
|
|
882
|
+
summaryArg.amountDue < 0 ? "payment.creditBalance" : "payment.amountDue"
|
|
883
|
+
)}
|
|
884
|
+
</div>
|
|
885
|
+
<div class="value value--total rightAlign amountCell">
|
|
886
|
+
${this.formatPrice(Math.abs(summaryArg.amountDue))}
|
|
887
|
+
</div>
|
|
888
|
+
</div>`;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* The advance payments the paid amount deducts, one line each, by the
|
|
893
|
+
* advance invoice's number and date and the day the payment was received,
|
|
894
|
+
* as far as the document states them. Each line is its own block, so a long
|
|
895
|
+
* list flows across pages.
|
|
896
|
+
*/
|
|
897
|
+
private renderAdvancePayments(
|
|
898
|
+
summaryArg: plugins.shared.IPaidAmountSummary
|
|
899
|
+
): TemplateResult[] {
|
|
900
|
+
return summaryArg.advancePayments.map(
|
|
901
|
+
(paymentArg) => html`<div class="advancePayment">
|
|
902
|
+
${this.translateKey("advancePayment.title")}${paymentArg.invoice
|
|
903
|
+
? html` ${this.translateKey("advancePayment.invoice")}
|
|
904
|
+
${paymentArg.invoice.documentId}${paymentArg.invoice.issueDate !== undefined
|
|
905
|
+
? html` ${this.translateKey("advancePayment.invoiceDate")}
|
|
906
|
+
${this.formatDay(paymentArg.invoice.issueDate)}`
|
|
907
|
+
: null}`
|
|
908
|
+
: null}${paymentArg.receivedOn !== undefined
|
|
909
|
+
? html`, ${this.translateKey("advancePayment.received")}
|
|
910
|
+
${this.formatDay(paymentArg.receivedOn)}`
|
|
911
|
+
: null}
|
|
912
|
+
</div>`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* What the document asks to be paid: the payment terms and the QR pay box
|
|
918
|
+
* for the gross total, or for the amount due when the document states a
|
|
919
|
+
* paid amount. When nothing is due, it says so, and names the credit
|
|
920
|
+
* balance when more was paid than the total; there are no terms and no QR
|
|
921
|
+
* pay box then.
|
|
922
|
+
*/
|
|
923
|
+
private renderPaymentRequest(
|
|
924
|
+
accountingDocArg: plugins.tsclass.finance.TAccountingDoc,
|
|
925
|
+
summaryArg: plugins.shared.IPaidAmountSummary | null
|
|
926
|
+
): TemplateResult {
|
|
927
|
+
if (summaryArg && summaryArg.amountDue <= 0) {
|
|
928
|
+
return html`<div class="infoBox">
|
|
929
|
+
<div>
|
|
930
|
+
<div>
|
|
931
|
+
<div class="label">${this.translateKey("invoice@@payment.terms")}</div>
|
|
932
|
+
<span>
|
|
933
|
+
${summaryArg.amountDue === 0
|
|
934
|
+
? this.translateKey("payment.nothingDue")
|
|
935
|
+
: html`${this.translateKey("payment.nothingDue.creditBalance")}
|
|
936
|
+
${this.formatPrice(-summaryArg.amountDue)}.`}
|
|
937
|
+
</span>
|
|
938
|
+
</div>
|
|
939
|
+
</div>
|
|
940
|
+
</div>`;
|
|
941
|
+
}
|
|
942
|
+
return html`
|
|
943
|
+
${this.renderPaymentTerms({
|
|
944
|
+
date: accountingDocArg.date,
|
|
945
|
+
dueInDays: accountingDocArg.dueInDays,
|
|
946
|
+
labelKey: "invoice@@payment.terms",
|
|
947
|
+
textKey: "invoice@@payment.terms.direct",
|
|
948
|
+
amountDue: summaryArg?.amountDue,
|
|
949
|
+
})}
|
|
950
|
+
${this.renderPaymentInfo({
|
|
951
|
+
from: accountingDocArg.from,
|
|
952
|
+
currency: accountingDocArg.currency,
|
|
953
|
+
amount: summaryArg ? summaryArg.amountDue : this.getTotalGross(),
|
|
954
|
+
reference: accountingDocArg.id,
|
|
955
|
+
})}
|
|
956
|
+
`;
|
|
957
|
+
}
|
|
958
|
+
|
|
560
959
|
/**
|
|
561
960
|
* The reverse-charge statement. § 14a UStG requires the words
|
|
562
961
|
* "Steuerschuldnerschaft des Leistungsempfängers": Abs. 5 for a domestic
|
|
@@ -577,6 +976,46 @@ export class DeContentInvoice extends DeesElement {
|
|
|
577
976
|
</div>`;
|
|
578
977
|
}
|
|
579
978
|
|
|
979
|
+
/**
|
|
980
|
+
* What makes a corrected invoice a correction (§ 31 Abs. 5 UStDV): its
|
|
981
|
+
* title, the invoice it corrects by number and issue date when the document
|
|
982
|
+
* states them ("spezifisch und eindeutig", Satz 2), and the details it
|
|
983
|
+
* supplies or corrects, in words. A received corrected invoice may name
|
|
984
|
+
* neither; then only the title is printed, and no reference is made up.
|
|
985
|
+
*/
|
|
986
|
+
private renderCorrection(): TemplateResult | null {
|
|
987
|
+
const accountingDoc = this.accountingDoc;
|
|
988
|
+
if (accountingDoc?.accountingDocType !== "corrected-invoice") {
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
const reference = accountingDoc.correctedInvoice;
|
|
992
|
+
const corrections = accountingDoc.corrections ?? [];
|
|
993
|
+
return html`<div class="correction">
|
|
994
|
+
<div class="correctionTitle">
|
|
995
|
+
${this.translateKey("correctedInvoice.title")}${reference
|
|
996
|
+
? html` ${this.translateKey("correctedInvoice.reference")}
|
|
997
|
+
${reference.documentId}${reference.issueDate !== undefined
|
|
998
|
+
? html` ${this.translateKey("correctedInvoice.referenceDate")}
|
|
999
|
+
${this.formatDay(reference.issueDate)}`
|
|
1000
|
+
: null}`
|
|
1001
|
+
: null}
|
|
1002
|
+
</div>
|
|
1003
|
+
${corrections.length > 0
|
|
1004
|
+
? html`<div class="correctionDetails">
|
|
1005
|
+
<div>${this.translateKey("correctedInvoice.corrections")}:</div>
|
|
1006
|
+
<ul>
|
|
1007
|
+
${corrections.map(
|
|
1008
|
+
(correctionArg) => html`<li>
|
|
1009
|
+
${this.translateKey(correctionDetailLabels[correctionArg.detail])}:
|
|
1010
|
+
${this.translateKey(correctionDefectLabels[correctionArg.defect])}
|
|
1011
|
+
</li>`
|
|
1012
|
+
)}
|
|
1013
|
+
</ul>
|
|
1014
|
+
</div>`
|
|
1015
|
+
: null}
|
|
1016
|
+
</div>`;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
580
1019
|
/** The earlier documents a correction answers, by number and, when known, by date. */
|
|
581
1020
|
private renderRelatedDocuments(): TemplateResult | null {
|
|
582
1021
|
const relatedDocuments = this.accountingDoc?.relatedDocuments;
|
|
@@ -610,8 +1049,8 @@ export class DeContentInvoice extends DeesElement {
|
|
|
610
1049
|
* pages and cannot split one, so a note is handed over token by token: a
|
|
611
1050
|
* paragraph in one piece could be taller than a page and would never fit.
|
|
612
1051
|
*/
|
|
613
|
-
private renderNotes(): TemplateResult | null {
|
|
614
|
-
const notes = (
|
|
1052
|
+
private renderNotes(notesArg: string[] | undefined): TemplateResult | null {
|
|
1053
|
+
const notes = (notesArg ?? [])
|
|
615
1054
|
.filter((noteArg): noteArg is string => typeof noteArg === "string")
|
|
616
1055
|
.map((noteArg) => noteArg.replace(/\r\n?/g, "\n").trim())
|
|
617
1056
|
.filter((noteArg) => noteArg.length > 0);
|
|
@@ -626,6 +1065,240 @@ export class DeContentInvoice extends DeesElement {
|
|
|
626
1065
|
)}`;
|
|
627
1066
|
}
|
|
628
1067
|
|
|
1068
|
+
/**
|
|
1069
|
+
* The document a reminder claims, as the claims table and the charges name
|
|
1070
|
+
* it: its kind and its number.
|
|
1071
|
+
*/
|
|
1072
|
+
private claimLabel(
|
|
1073
|
+
reminderArg: plugins.tsclass.finance.TPaymentReminder,
|
|
1074
|
+
documentIdArg: string
|
|
1075
|
+
): string {
|
|
1076
|
+
const claim = reminderArg.claims.find(
|
|
1077
|
+
(claimArg) => claimArg.documentId === documentIdArg
|
|
1078
|
+
);
|
|
1079
|
+
if (!claim) {
|
|
1080
|
+
return documentIdArg;
|
|
1081
|
+
}
|
|
1082
|
+
// a kind this version does not know is named as an invoice, as the letterhead and the content do
|
|
1083
|
+
const label =
|
|
1084
|
+
claimDocumentLabels[claim.documentType] ?? claimDocumentLabels.invoice;
|
|
1085
|
+
return `${this.translateKey(label)} ${documentIdArg}`;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* The ground of the claim: every reminded document with its number, its
|
|
1090
|
+
* date, the date it fell due when it stated one, its gross total and what is
|
|
1091
|
+
* still open on it.
|
|
1092
|
+
*/
|
|
1093
|
+
private renderReminderClaims(
|
|
1094
|
+
reminderArg: plugins.tsclass.finance.TPaymentReminder
|
|
1095
|
+
): TemplateResult {
|
|
1096
|
+
return html`<div class="claimGrid topLine dataHeader">
|
|
1097
|
+
<div class="lineItem descriptionCell">
|
|
1098
|
+
${this.translateKey("paymentReminder@@claim.document")}
|
|
1099
|
+
</div>
|
|
1100
|
+
<div class="lineItem rightAlign issueDateCell">
|
|
1101
|
+
${this.translateKey("paymentReminder@@claim.issueDate")}
|
|
1102
|
+
</div>
|
|
1103
|
+
<div class="lineItem rightAlign dueDateCell">
|
|
1104
|
+
${this.translateKey("paymentReminder@@claim.dueDate")}
|
|
1105
|
+
</div>
|
|
1106
|
+
<div class="lineItem rightAlign grossCell">
|
|
1107
|
+
${this.translateKey("paymentReminder@@claim.totalGross")}
|
|
1108
|
+
</div>
|
|
1109
|
+
<div class="lineItem rightAlign amountCell">
|
|
1110
|
+
${this.translateKey("paymentReminder@@claim.outstanding")}
|
|
1111
|
+
</div>
|
|
1112
|
+
</div>
|
|
1113
|
+
${reminderArg.claims.map(
|
|
1114
|
+
(claimArg) => html`<div class="claimGrid needsDataHeader">
|
|
1115
|
+
<div class="lineItem descriptionCell">
|
|
1116
|
+
${this.claimLabel(reminderArg, claimArg.documentId)}
|
|
1117
|
+
</div>
|
|
1118
|
+
<div class="lineItem rightAlign issueDateCell">
|
|
1119
|
+
${this.formatDay(claimArg.issueDate)}
|
|
1120
|
+
</div>
|
|
1121
|
+
<div class="lineItem rightAlign dueDateCell">
|
|
1122
|
+
${claimArg.dueDate !== undefined
|
|
1123
|
+
? this.formatDay(claimArg.dueDate)
|
|
1124
|
+
: "–"}
|
|
1125
|
+
</div>
|
|
1126
|
+
<div class="lineItem rightAlign grossCell">
|
|
1127
|
+
${this.formatPrice(claimArg.totalGross)}
|
|
1128
|
+
</div>
|
|
1129
|
+
<div class="lineItem rightAlign amountCell">
|
|
1130
|
+
${this.formatPrice(claimArg.outstandingAmount)}
|
|
1131
|
+
</div>
|
|
1132
|
+
</div>`
|
|
1133
|
+
)}`;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* What a charge rests on, as the reminder states it: default interest its
|
|
1138
|
+
* computation (principal, rate a year with the base rate and the surcharge
|
|
1139
|
+
* points of § 288 Abs. 1 or 2 BGB, first and last day), the default lump sum
|
|
1140
|
+
* its provision, and every charge the claim it accrues on. A cost with lump
|
|
1141
|
+
* sums credited against it lists them (§ 288 Abs. 5 Satz 3 BGB); its amount
|
|
1142
|
+
* is already the cost less them.
|
|
1143
|
+
*/
|
|
1144
|
+
private renderChargeBasis(
|
|
1145
|
+
reminderArg: plugins.tsclass.finance.TPaymentReminder,
|
|
1146
|
+
chargeArg: plugins.tsclass.finance.TPaymentReminderCharge
|
|
1147
|
+
): TemplateResult | null {
|
|
1148
|
+
switch (chargeArg.chargeType) {
|
|
1149
|
+
case "default-interest": {
|
|
1150
|
+
const interest = chargeArg.interest;
|
|
1151
|
+
return html`<div class="chargeBasis">
|
|
1152
|
+
${this.claimLabel(reminderArg, chargeArg.claimDocumentId)}:
|
|
1153
|
+
${this.formatPrice(interest.principal)} ×
|
|
1154
|
+
${this.formatPercent(interest.ratePercent)}
|
|
1155
|
+
${this.translateKey("paymentReminder@@interest.perYear")}
|
|
1156
|
+
(${this.translateKey("paymentReminder@@interest.baseRate")}
|
|
1157
|
+
${this.formatPercent(interest.baseRatePercent)}
|
|
1158
|
+
${this.translateKey("paymentReminder@@interest.plus")}
|
|
1159
|
+
${interest.surchargePoints}
|
|
1160
|
+
${this.translateKey("paymentReminder@@interest.points")},
|
|
1161
|
+
<span class="citation"
|
|
1162
|
+
>${plugins.shared.defaultInterestCitations[interest.legalBasis]}</span
|
|
1163
|
+
>),
|
|
1164
|
+
${this.formatDay(interest.from)} – ${this.formatDay(interest.to)}
|
|
1165
|
+
</div>`;
|
|
1166
|
+
}
|
|
1167
|
+
case "default-lump-sum":
|
|
1168
|
+
return html`<div class="chargeBasis">
|
|
1169
|
+
${this.claimLabel(reminderArg, chargeArg.claimDocumentId)},
|
|
1170
|
+
<span class="citation"
|
|
1171
|
+
>${plugins.shared.DEFAULT_LUMP_SUM_CITATION}</span
|
|
1172
|
+
>
|
|
1173
|
+
</div>`;
|
|
1174
|
+
case "reminder-fee":
|
|
1175
|
+
case "other-cost": {
|
|
1176
|
+
const offsets = chargeArg.lumpSumOffsets ?? [];
|
|
1177
|
+
if (chargeArg.claimDocumentId === undefined && offsets.length === 0) {
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
return html`${chargeArg.claimDocumentId !== undefined
|
|
1181
|
+
? html`<div class="chargeBasis">
|
|
1182
|
+
${this.claimLabel(reminderArg, chargeArg.claimDocumentId)}
|
|
1183
|
+
</div>`
|
|
1184
|
+
: null}
|
|
1185
|
+
${offsets.map(
|
|
1186
|
+
(offsetArg) => html`<div class="chargeBasis">
|
|
1187
|
+
${this.translateKey("paymentReminder@@charge.lumpSumOffset")}
|
|
1188
|
+
${this.claimLabel(reminderArg, offsetArg.claimDocumentId)}:
|
|
1189
|
+
${this.formatPrice(offsetArg.amount)}
|
|
1190
|
+
(<span class="citation"
|
|
1191
|
+
>${plugins.shared.LUMP_SUM_OFFSET_CITATION}</span
|
|
1192
|
+
>)
|
|
1193
|
+
</div>`
|
|
1194
|
+
)}`;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/** One row per charge: its description, what it rests on, and its amount. */
|
|
1200
|
+
private renderReminderCharges(
|
|
1201
|
+
reminderArg: plugins.tsclass.finance.TPaymentReminder
|
|
1202
|
+
): TemplateResult | null {
|
|
1203
|
+
if (reminderArg.charges.length === 0) {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
return html`<div class="chargeGrid topLine dataHeader">
|
|
1207
|
+
<div class="lineItem">
|
|
1208
|
+
${this.translateKey("paymentReminder@@charge.description")}
|
|
1209
|
+
</div>
|
|
1210
|
+
<div class="lineItem rightAlign amountCell">
|
|
1211
|
+
${this.translateKey("paymentReminder@@charge.amount")}
|
|
1212
|
+
</div>
|
|
1213
|
+
</div>
|
|
1214
|
+
${reminderArg.charges.map(
|
|
1215
|
+
(chargeArg) => html`<div class="chargeGrid needsDataHeader">
|
|
1216
|
+
<div class="lineItem chargeText">
|
|
1217
|
+
<div>${chargeArg.description}</div>
|
|
1218
|
+
${this.renderChargeBasis(reminderArg, chargeArg)}
|
|
1219
|
+
</div>
|
|
1220
|
+
<div class="lineItem rightAlign amountCell">
|
|
1221
|
+
${this.formatPrice(chargeArg.amount)}
|
|
1222
|
+
</div>
|
|
1223
|
+
</div>`
|
|
1224
|
+
)}`;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/** The sums of a reminder: the open claims, the charges and the total payable, without VAT. */
|
|
1228
|
+
private renderReminderSums(
|
|
1229
|
+
totalsArg: plugins.shared.IPaymentReminderTotals,
|
|
1230
|
+
hasChargesArg: boolean
|
|
1231
|
+
): TemplateResult {
|
|
1232
|
+
return html`<div class="sums">
|
|
1233
|
+
<div class="sumline">
|
|
1234
|
+
<div class="label">
|
|
1235
|
+
${this.translateKey("paymentReminder@@paymentReminder.sum.claims")}
|
|
1236
|
+
</div>
|
|
1237
|
+
<div class="value rightAlign amountCell">
|
|
1238
|
+
${this.formatPrice(totalsArg.claimsOutstanding)}
|
|
1239
|
+
</div>
|
|
1240
|
+
</div>
|
|
1241
|
+
${hasChargesArg
|
|
1242
|
+
? html`<div class="sumline">
|
|
1243
|
+
<div class="label">
|
|
1244
|
+
${this.translateKey("paymentReminder@@paymentReminder.sum.charges")}
|
|
1245
|
+
</div>
|
|
1246
|
+
<div class="value rightAlign amountCell">
|
|
1247
|
+
${this.formatPrice(totalsArg.charges)}
|
|
1248
|
+
</div>
|
|
1249
|
+
</div>`
|
|
1250
|
+
: null}
|
|
1251
|
+
<div class="sumline">
|
|
1252
|
+
<div class="label">
|
|
1253
|
+
${this.translateKey("paymentReminder@@paymentReminder.sum.payable")}
|
|
1254
|
+
</div>
|
|
1255
|
+
<div class="value value--total rightAlign amountCell">
|
|
1256
|
+
${this.formatPrice(totalsArg.payable)}
|
|
1257
|
+
</div>
|
|
1258
|
+
</div>
|
|
1259
|
+
</div>`;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* A payment reminder: the issuer's intro or the standard one, the reminded
|
|
1264
|
+
* documents, the charges, the sums, the notes, the new payment deadline and
|
|
1265
|
+
* the QR pay box for the total payable with the reminder's number.
|
|
1266
|
+
*/
|
|
1267
|
+
private renderPaymentReminder(
|
|
1268
|
+
reminderArg: plugins.tsclass.finance.TPaymentReminder
|
|
1269
|
+
): TemplateResult {
|
|
1270
|
+
const totals = plugins.shared.getPaymentReminderTotals(reminderArg);
|
|
1271
|
+
return html`
|
|
1272
|
+
<div>
|
|
1273
|
+
${reminderArg.topText ??
|
|
1274
|
+
this.translateKey("paymentReminder@@introStatement")}
|
|
1275
|
+
</div>
|
|
1276
|
+
${this.renderReminderClaims(reminderArg)}
|
|
1277
|
+
${this.renderReminderCharges(reminderArg)}
|
|
1278
|
+
${this.renderReminderSums(totals, reminderArg.charges.length > 0)}
|
|
1279
|
+
<div class="divider"></div>
|
|
1280
|
+
|
|
1281
|
+
<!-- NOTES -->
|
|
1282
|
+
${this.renderNotes(reminderArg.notes)}
|
|
1283
|
+
|
|
1284
|
+
<!-- PAYMENT DEADLINE -->
|
|
1285
|
+
${this.renderPaymentTerms({
|
|
1286
|
+
date: reminderArg.date,
|
|
1287
|
+
dueInDays: reminderArg.dueInDays,
|
|
1288
|
+
labelKey: "paymentReminder@@paymentReminder.deadline",
|
|
1289
|
+
textKey: "paymentReminder@@paymentReminder.deadline.text",
|
|
1290
|
+
})}
|
|
1291
|
+
|
|
1292
|
+
<!-- PAYMENT INFO -->
|
|
1293
|
+
${this.renderPaymentInfo({
|
|
1294
|
+
from: reminderArg.from,
|
|
1295
|
+
currency: reminderArg.currency,
|
|
1296
|
+
amount: totals.payable,
|
|
1297
|
+
reference: reminderArg.id,
|
|
1298
|
+
})}
|
|
1299
|
+
`;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
629
1302
|
private renderReferencedContract(): TemplateResult | null {
|
|
630
1303
|
return null;
|
|
631
1304
|
// return this.documentSettings.enableInvoiceContractRefSection &&
|
|
@@ -648,29 +1321,46 @@ export class DeContentInvoice extends DeesElement {
|
|
|
648
1321
|
|
|
649
1322
|
public async attachInvoiceDom() {
|
|
650
1323
|
const contentNodes = await this.getContentNodes();
|
|
1324
|
+
const paymentReminder = this.paymentReminder;
|
|
1325
|
+
if (paymentReminder) {
|
|
1326
|
+
render(this.renderPaymentReminder(paymentReminder), contentNodes.currentContent);
|
|
1327
|
+
this.fitColumns(
|
|
1328
|
+
contentNodes.currentContent,
|
|
1329
|
+
".claimGrid.dataHeader",
|
|
1330
|
+
reminderColumns
|
|
1331
|
+
);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
651
1334
|
const accountingDoc = this.accountingDoc;
|
|
1335
|
+
// throws when the advance payments do not add up to the paid amount
|
|
1336
|
+
const paidAmountSummary = accountingDoc
|
|
1337
|
+
? plugins.shared.getPaidAmountSummary(accountingDoc, this.getTotalGross())
|
|
1338
|
+
: null;
|
|
652
1339
|
render(
|
|
653
1340
|
html`
|
|
1341
|
+
${this.renderCorrection()}
|
|
654
1342
|
<div>${this.translateKey(this.docContent.introStatement)}</div>
|
|
655
1343
|
${this.renderRelatedDocuments()}
|
|
656
1344
|
<div class="grid topLine dataHeader">
|
|
657
1345
|
<div class="lineItem rightAlign">
|
|
658
1346
|
${this.translateKey("invoice@@item.position")}
|
|
659
1347
|
</div>
|
|
660
|
-
<div class="lineItem">
|
|
1348
|
+
<div class="lineItem descriptionCell">
|
|
661
1349
|
${this.translateKey("invoice@@description")}
|
|
662
1350
|
</div>
|
|
663
|
-
<div class="lineItem rightAlign">
|
|
1351
|
+
<div class="lineItem rightAlign quantityCell">
|
|
664
1352
|
${this.translateKey("invoice@@quantity")}
|
|
665
1353
|
</div>
|
|
666
|
-
<div class="lineItem"
|
|
667
|
-
|
|
1354
|
+
<div class="lineItem unitCell">
|
|
1355
|
+
${this.translateKey("invoice@@unit.type")}
|
|
1356
|
+
</div>
|
|
1357
|
+
<div class="lineItem rightAlign unitPriceCell">
|
|
668
1358
|
${this.translateKey("invoice@@price.unit.net")}
|
|
669
1359
|
</div>
|
|
670
1360
|
<div class="lineItem rightAlign">
|
|
671
1361
|
${this.translateKey("invoice@@vat.short")}
|
|
672
1362
|
</div>
|
|
673
|
-
<div class="lineItem rightAlign">
|
|
1363
|
+
<div class="lineItem rightAlign amountCell">
|
|
674
1364
|
${this.translateKey("invoice@@price.total.net")}
|
|
675
1365
|
</div>
|
|
676
1366
|
</div>
|
|
@@ -678,16 +1368,24 @@ export class DeContentInvoice extends DeesElement {
|
|
|
678
1368
|
(invoiceItem, index) => html`
|
|
679
1369
|
<div class="grid needsDataHeader">
|
|
680
1370
|
<div class="lineItem rightAlign">${index + 1}</div>
|
|
681
|
-
<div class="lineItem">${invoiceItem.name}</div>
|
|
682
|
-
<div class="lineItem rightAlign"
|
|
683
|
-
|
|
684
|
-
|
|
1371
|
+
<div class="lineItem descriptionCell">${invoiceItem.name}</div>
|
|
1372
|
+
<div class="lineItem rightAlign quantityCell">
|
|
1373
|
+
${this.formatQuantity(invoiceItem.unitQuantity)}
|
|
1374
|
+
</div>
|
|
1375
|
+
<div class="lineItem unitCell">
|
|
1376
|
+
${plugins.shared.unitName(
|
|
1377
|
+
this.documentSettings.languageCode,
|
|
1378
|
+
invoiceItem.unitType,
|
|
1379
|
+
invoiceItem.unitQuantity
|
|
1380
|
+
)}
|
|
1381
|
+
</div>
|
|
1382
|
+
<div class="lineItem rightAlign unitPriceCell">
|
|
685
1383
|
${this.formatPrice(invoiceItem.unitNetPrice)}
|
|
686
1384
|
</div>
|
|
687
1385
|
<div class="lineItem rightAlign">
|
|
688
1386
|
${invoiceItem.vatPercentage}%
|
|
689
1387
|
</div>
|
|
690
|
-
<div class="lineItem rightAlign">
|
|
1388
|
+
<div class="lineItem rightAlign amountCell">
|
|
691
1389
|
${this.formatPrice(
|
|
692
1390
|
invoiceItem.unitQuantity * invoiceItem.unitNetPrice
|
|
693
1391
|
)}
|
|
@@ -700,7 +1398,7 @@ export class DeContentInvoice extends DeesElement {
|
|
|
700
1398
|
<div class="label">
|
|
701
1399
|
${this.translateKey("invoice@@sum.total.net")}
|
|
702
1400
|
</div>
|
|
703
|
-
<div class="value value--total rightAlign">
|
|
1401
|
+
<div class="value value--total rightAlign amountCell">
|
|
704
1402
|
${this.formatPrice(this.getTotalNet())}
|
|
705
1403
|
</div>
|
|
706
1404
|
</div>
|
|
@@ -722,7 +1420,7 @@ export class DeContentInvoice extends DeesElement {
|
|
|
722
1420
|
`
|
|
723
1421
|
: html``}
|
|
724
1422
|
</div>
|
|
725
|
-
<div class="value rightAlign">
|
|
1423
|
+
<div class="value rightAlign amountCell">
|
|
726
1424
|
${this.formatPrice(vatGroupArg.vatAmountSum)}
|
|
727
1425
|
</div>
|
|
728
1426
|
</div>
|
|
@@ -730,11 +1428,13 @@ export class DeContentInvoice extends DeesElement {
|
|
|
730
1428
|
})}
|
|
731
1429
|
<div class="sumline">
|
|
732
1430
|
<div class="label">${this.translateKey("invoice@@totalGross")}</div>
|
|
733
|
-
<div class="value value--total rightAlign">
|
|
1431
|
+
<div class="value value--total rightAlign amountCell">
|
|
734
1432
|
${this.formatPrice(this.getTotalGross())}
|
|
735
1433
|
</div>
|
|
736
1434
|
</div>
|
|
1435
|
+
${paidAmountSummary ? this.renderPaidAmountSums(paidAmountSummary) : null}
|
|
737
1436
|
</div>
|
|
1437
|
+
${paidAmountSummary ? this.renderAdvancePayments(paidAmountSummary) : null}
|
|
738
1438
|
<div class="divider"></div>
|
|
739
1439
|
|
|
740
1440
|
${accountingDoc?.reverseCharge ? this.renderReverseChargeNote() : ``}
|
|
@@ -743,19 +1443,72 @@ export class DeContentInvoice extends DeesElement {
|
|
|
743
1443
|
${this.renderReferencedContract()}
|
|
744
1444
|
|
|
745
1445
|
<!-- NOTES -->
|
|
746
|
-
${this.renderNotes()}
|
|
1446
|
+
${this.renderNotes(accountingDoc?.notes)}
|
|
747
1447
|
|
|
748
|
-
<!-- PAYMENT TERMS -->
|
|
1448
|
+
<!-- PAYMENT TERMS AND PAYMENT INFO -->
|
|
749
1449
|
${accountingDoc && this.docContent.requestsPayment
|
|
750
|
-
? this.
|
|
751
|
-
: null}
|
|
752
|
-
|
|
753
|
-
<!-- PAYMENT INFO -->
|
|
754
|
-
${accountingDoc && this.docContent.requestsPayment
|
|
755
|
-
? this.renderPaymentInfo(accountingDoc)
|
|
1450
|
+
? this.renderPaymentRequest(accountingDoc, paidAmountSummary)
|
|
756
1451
|
: null}
|
|
757
1452
|
`,
|
|
758
1453
|
contentNodes.currentContent
|
|
759
1454
|
);
|
|
1455
|
+
this.fitColumns(
|
|
1456
|
+
contentNodes.currentContent,
|
|
1457
|
+
".grid.dataHeader",
|
|
1458
|
+
positionColumns
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Sizes the columns of a table to their widest cell. Every row is a grid of
|
|
1464
|
+
* its own, so the rows only line up when a column has one width for all of
|
|
1465
|
+
* them; each page renders every row before it is trimmed, so every page
|
|
1466
|
+
* measures the same widths.
|
|
1467
|
+
*
|
|
1468
|
+
* A column is never narrower than its minimum. The widths the columns ask
|
|
1469
|
+
* for beyond their minimums are granted out of the row's width less its
|
|
1470
|
+
* fixed columns and the minimum of its flexible column (the description),
|
|
1471
|
+
* in equal shares, so the table always fits the page. A cell whose text is
|
|
1472
|
+
* still wider than its column, such as an amount beyond hundreds of
|
|
1473
|
+
* billions, is set in a smaller font, down to 8 px, and wraps inside its
|
|
1474
|
+
* column once that is not enough, so it never runs into its neighbour.
|
|
1475
|
+
*/
|
|
1476
|
+
private fitColumns(
|
|
1477
|
+
containerArg: HTMLElement,
|
|
1478
|
+
headerRowSelectorArg: string,
|
|
1479
|
+
layoutArg: IColumnLayout
|
|
1480
|
+
): void {
|
|
1481
|
+
const cellsOf = (columnArg: IFittedColumn): HTMLElement[] =>
|
|
1482
|
+
Array.from(containerArg.querySelectorAll<HTMLElement>(columnArg.cellSelector));
|
|
1483
|
+
for (const column of layoutArg.columns) {
|
|
1484
|
+
this.style.removeProperty(column.widthProperty);
|
|
1485
|
+
for (const cell of cellsOf(column)) {
|
|
1486
|
+
cell.style.removeProperty("font-size");
|
|
1487
|
+
cell.style.removeProperty("white-space");
|
|
1488
|
+
cell.style.removeProperty("overflow-wrap");
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
const headerRow = containerArg.querySelector<HTMLElement>(headerRowSelectorArg);
|
|
1492
|
+
if (!headerRow || headerRow.clientWidth === 0) {
|
|
1493
|
+
// not laid out: the columns keep their minimums
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
const wanted = layoutArg.columns.map((columnArg) =>
|
|
1497
|
+
cellsOf(columnArg).reduce(
|
|
1498
|
+
(widestArg, cellArg) => Math.max(widestArg, neededWidth(cellArg)),
|
|
1499
|
+
columnArg.minWidth
|
|
1500
|
+
)
|
|
1501
|
+
);
|
|
1502
|
+
const widths = shareWidth(
|
|
1503
|
+
wanted,
|
|
1504
|
+
layoutArg.columns.map((columnArg) => columnArg.minWidth),
|
|
1505
|
+
headerRow.clientWidth - layoutArg.fixedWidth - layoutArg.flexibleMinWidth
|
|
1506
|
+
);
|
|
1507
|
+
layoutArg.columns.forEach((columnArg, index) => {
|
|
1508
|
+
this.style.setProperty(columnArg.widthProperty, `${widths[index]}px`);
|
|
1509
|
+
for (const cell of cellsOf(columnArg)) {
|
|
1510
|
+
fitCellText(cell, widths[index]);
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
760
1513
|
}
|
|
761
1514
|
}
|