494 lines
13 KiB
TypeScript
494 lines
13 KiB
TypeScript
/**
|
|
* content for invoices
|
|
*/
|
|
|
|
import {
|
|
DeesElement,
|
|
property,
|
|
html,
|
|
customElement,
|
|
type TemplateResult,
|
|
css,
|
|
render,
|
|
domtools,
|
|
} from "@design.estate/dees-element";
|
|
import * as plugins from "../plugins.js";
|
|
|
|
import { dedocumentSharedStyle } from "../style.js";
|
|
import type { TranslationKey } from "ts_shared/translation.js";
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"dedocument-contentinvoice": DeContentInvoice;
|
|
}
|
|
}
|
|
|
|
@customElement("dedocument-contentinvoice")
|
|
export class DeContentInvoice extends DeesElement {
|
|
public static demo = () => html`
|
|
<style>
|
|
.demoContainer {
|
|
background: white;
|
|
padding: 50px;
|
|
}
|
|
</style>
|
|
<div class="demoContainer">
|
|
<dedocument-contentinvoice></dedocument-contentinvoice>
|
|
</div>
|
|
`;
|
|
|
|
@property({
|
|
type: Object,
|
|
reflect: true,
|
|
})
|
|
public letterData: plugins.tsclass.business.ILetter;
|
|
|
|
@property({
|
|
type: Object,
|
|
reflect: true,
|
|
})
|
|
public documentSettings: plugins.shared.interfaces.IDocumentSettings;
|
|
|
|
constructor() {
|
|
super();
|
|
domtools.DomTools.setupDomTools();
|
|
}
|
|
|
|
public static styles = [
|
|
domtools.elementBasic.staticStyles,
|
|
dedocumentSharedStyle,
|
|
css`
|
|
.trimmedContent {
|
|
display: none;
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: 40px auto 50px 50px 100px 50px 100px;
|
|
}
|
|
|
|
.topLine {
|
|
margin-top: 5px;
|
|
background: #e7e7e7;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.lineItem {
|
|
font-size: 12px;
|
|
padding: 5px;
|
|
border-right: 1px dashed #ccc;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.lineItem:last-child {
|
|
border-right: none;
|
|
}
|
|
|
|
.value.rightAlign,
|
|
.lineItem.rightAlign {
|
|
text-align: right;
|
|
}
|
|
|
|
.invoiceLine {
|
|
background: #ffffff00;
|
|
border-bottom: 1px dotted #ccc;
|
|
}
|
|
|
|
.invoiceLine.highlighted {
|
|
transition: background 0.2s;
|
|
background: #ffc18f;
|
|
border: 1px solid #ff9d4d;
|
|
box-sizing: content-box;
|
|
}
|
|
|
|
.sums {
|
|
margin-top: 5px;
|
|
font-size: 12px;
|
|
padding-left: 50%;
|
|
}
|
|
|
|
.sums .sumline {
|
|
margin-top: 3px;
|
|
display: grid;
|
|
grid-template-columns: auto 100px;
|
|
}
|
|
|
|
.sums .sumline .label {
|
|
padding: 2px 5px;
|
|
border-right: 1px solid #ccc;
|
|
text-align: right;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.sums .sumline .value {
|
|
padding: 2px 5px;
|
|
}
|
|
|
|
.sums .sumline .value--total {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.divider {
|
|
margin-top: 8px;
|
|
border-top: 1px dotted #ccc;
|
|
}
|
|
|
|
.taxNote {
|
|
font-size: 12px;
|
|
padding: 4px;
|
|
background: #eeeeeb;
|
|
text-align: center;
|
|
}
|
|
|
|
.infoBox {
|
|
margin-top: 22px;
|
|
line-height: 1.4em;
|
|
}
|
|
|
|
.infoBox .label {
|
|
padding-bottom: 2px;
|
|
font-weight: bold;
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render(): TemplateResult {
|
|
return html`
|
|
<div class="trimmedContent"></div>
|
|
<div class="repeatedContent"></div>
|
|
<div class="currentContent"></div>
|
|
`;
|
|
}
|
|
|
|
protected formatPrice(
|
|
value: number,
|
|
currency = "EUR",
|
|
lang = "de-DE"
|
|
): string {
|
|
return new Intl.NumberFormat(lang, {
|
|
style: "currency",
|
|
currency,
|
|
}).format(value);
|
|
}
|
|
|
|
public getTotalNet = (): number => {
|
|
let totalNet = 0;
|
|
|
|
if (!this.letterData) {
|
|
return totalNet;
|
|
}
|
|
|
|
for (const item of this.letterData.content.invoiceData.items) {
|
|
totalNet += item.unitNetPrice * item.unitQuantity;
|
|
}
|
|
return totalNet;
|
|
};
|
|
|
|
public getTotalGross = (): number => {
|
|
let totalVat = 0;
|
|
|
|
if (!this.letterData) {
|
|
return totalVat;
|
|
}
|
|
|
|
for (const taxgroup of this.getVatGroups()) {
|
|
totalVat += taxgroup.vatAmountSum;
|
|
}
|
|
return this.getTotalNet() + totalVat;
|
|
};
|
|
|
|
public getVatGroups = () => {
|
|
const vatGroups: {
|
|
vatPercentage: number;
|
|
items: plugins.tsclass.finance.IInvoice["items"];
|
|
vatAmountSum: number;
|
|
}[] = [];
|
|
|
|
if (!this.letterData) {
|
|
return vatGroups;
|
|
}
|
|
|
|
const taxAmounts: number[] = [];
|
|
for (const item of this.letterData.content.invoiceData.items) {
|
|
taxAmounts.includes(item.vatPercentage)
|
|
? null
|
|
: taxAmounts.push(item.vatPercentage);
|
|
}
|
|
|
|
for (const taxAmount of taxAmounts) {
|
|
const matchingItems = this.letterData.content.invoiceData.items.filter(
|
|
(itemArg) => itemArg.vatPercentage === taxAmount
|
|
);
|
|
let sum = 0;
|
|
for (const matchingItem of matchingItems) {
|
|
sum +=
|
|
matchingItem.unitNetPrice *
|
|
matchingItem.unitQuantity *
|
|
(taxAmount / 100);
|
|
}
|
|
vatGroups.push({
|
|
items: matchingItems,
|
|
vatPercentage: taxAmount,
|
|
vatAmountSum: Math.round(sum * 100) / 100,
|
|
});
|
|
}
|
|
return vatGroups.sort((a, b) => b.vatPercentage - a.vatPercentage);
|
|
};
|
|
|
|
public async getContentNodes() {
|
|
await this.elementDomReady;
|
|
return {
|
|
currentContent: this.shadowRoot.querySelector(
|
|
".currentContent"
|
|
) as HTMLElement,
|
|
trimmedContent: this.shadowRoot.querySelector(
|
|
".trimmedContent"
|
|
) as HTMLElement,
|
|
repeatedContent: this.shadowRoot.querySelector(
|
|
".repeatedContent"
|
|
) as HTMLElement,
|
|
};
|
|
}
|
|
|
|
public async getContentLength() {
|
|
await this.elementDomReady;
|
|
return (await this.getContentNodes()).currentContent.children.length;
|
|
}
|
|
|
|
public async trimEndByOne() {
|
|
await this.elementDomReady;
|
|
this.shadowRoot
|
|
.querySelector(".trimmedContent")
|
|
.append(
|
|
(await this.getContentNodes()).currentContent.children.item(
|
|
(await this.getContentNodes()).currentContent.children.length - 1
|
|
)
|
|
);
|
|
}
|
|
|
|
public async trimStartToOffset(contentOffsetArg: number) {
|
|
await this.elementDomReady;
|
|
const beginningLength = (await this.getContentNodes()).currentContent
|
|
.children.length;
|
|
while (
|
|
(await this.getContentNodes()).currentContent.children.length !==
|
|
beginningLength - contentOffsetArg
|
|
) {
|
|
(await this.getContentNodes()).trimmedContent.append(
|
|
(await this.getContentNodes()).currentContent.children.item(0)
|
|
);
|
|
}
|
|
if (
|
|
(await this.getContentNodes()).currentContent.children
|
|
.item(0)
|
|
.classList.contains("needsDataHeader")
|
|
) {
|
|
const trimmedContent = (await this.getContentNodes()).trimmedContent;
|
|
let startPoint = trimmedContent.children.length;
|
|
while (startPoint > 0) {
|
|
const element = trimmedContent.children.item(startPoint - 1);
|
|
if (element.classList.contains("dataHeader")) {
|
|
(await this.getContentNodes()).repeatedContent.append(element);
|
|
break;
|
|
}
|
|
startPoint--;
|
|
}
|
|
}
|
|
}
|
|
|
|
public translateKey(key: TranslationKey): string {
|
|
return plugins.shared.translation.translate(
|
|
this.documentSettings.languageCode,
|
|
key
|
|
);
|
|
}
|
|
|
|
public async firstUpdated(
|
|
_changedProperties: Map<string | number | symbol, unknown>
|
|
) {
|
|
super.firstUpdated(_changedProperties);
|
|
this.attachInvoiceDom();
|
|
}
|
|
|
|
private renderPaymentTerms(): TemplateResult {
|
|
return html`<div class="infoBox">
|
|
<div>
|
|
<div>
|
|
<div class="label">
|
|
${this.translateKey("invoice@@payment.terms")}
|
|
</div>
|
|
<span>
|
|
${this.translateKey("invoice@@payment.terms.direct")}
|
|
${new Intl.DateTimeFormat(this.documentSettings.languageCode, {
|
|
dateStyle: this.documentSettings.dateStyle,
|
|
}).format(
|
|
new Date(this.letterData.date).setDate(
|
|
new Date(this.letterData.date).getDate() +
|
|
this.letterData?.content.invoiceData.dueInDays
|
|
)
|
|
)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
private renderPaymentInfo(): TemplateResult {
|
|
const bic =
|
|
this.letterData?.content.invoiceData.billedBy.sepaConnection.bic;
|
|
const name = this.letterData?.content.invoiceData.billedBy.name;
|
|
const iban =
|
|
this.letterData?.content.invoiceData.billedBy.sepaConnection.iban;
|
|
const currency = this.letterData?.content.invoiceData.currency;
|
|
const totalGross = this.getTotalGross();
|
|
const reference = this.letterData?.content.invoiceData.id;
|
|
|
|
return html`<div class="infoBox">
|
|
<div>
|
|
<div>
|
|
<div class="label">${this.translateKey("invoice@@payment.qr")}</div>
|
|
<span> ${this.translateKey("invoice@@payment.qr.description")} </span>
|
|
</div>
|
|
<dedocument-paymentcode
|
|
bic="${bic}"
|
|
name="${name}"
|
|
iban="${iban}"
|
|
currency="${currency}"
|
|
totalGross="${totalGross}"
|
|
reference="${reference}"
|
|
/>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
private renderReferencedContract(): TemplateResult {
|
|
return this.documentSettings.enableInvoiceContractRefSection &&
|
|
this.letterData?.content?.contractData?.contractDate
|
|
? html`
|
|
<div class="infoBox">
|
|
<div class="label">
|
|
${this.translateKey("invoice@@referencedContract")}
|
|
</div>
|
|
${this.translateKey("invoice@@referencedContract.text")}
|
|
${new Intl.DateTimeFormat(this.documentSettings.languageCode, {
|
|
dateStyle: this.documentSettings.dateStyle,
|
|
}).format(
|
|
new Date(this.letterData?.content.contractData.contractDate)
|
|
)}.
|
|
</div>
|
|
`
|
|
: null;
|
|
}
|
|
|
|
public async attachInvoiceDom() {
|
|
const contentNodes = await this.getContentNodes();
|
|
render(
|
|
html`
|
|
<div>${this.translateKey("invoice@@introStatement")}</div>
|
|
<div class="grid topLine dataHeader">
|
|
<div class="lineItem rightAlign">
|
|
${this.translateKey("invoice@@item.position")}
|
|
</div>
|
|
<div class="lineItem">
|
|
${this.translateKey("invoice@@description")}
|
|
</div>
|
|
<div class="lineItem rightAlign">
|
|
${this.translateKey("invoice@@quantity")}
|
|
</div>
|
|
<div class="lineItem">${this.translateKey("invoice@@unit.type")}</div>
|
|
<div class="lineItem rightAlign">
|
|
${this.translateKey("invoice@@price.unit.net")}
|
|
</div>
|
|
<div class="lineItem rightAlign">
|
|
${this.translateKey("invoice@@vat.short")}
|
|
</div>
|
|
<div class="lineItem rightAlign">
|
|
${this.translateKey("invoice@@price.total.net")}
|
|
</div>
|
|
</div>
|
|
${this.letterData?.content.invoiceData?.items?.map(
|
|
(invoiceItem, index) => html`
|
|
<div class="grid needsDataHeader">
|
|
<div class="lineItem rightAlign">${index + 1}</div>
|
|
<div class="lineItem">${invoiceItem.name}</div>
|
|
<div class="lineItem rightAlign">${invoiceItem.unitQuantity}</div>
|
|
<div class="lineItem">${invoiceItem.unitType}</div>
|
|
<div class="lineItem rightAlign">
|
|
${this.formatPrice(invoiceItem.unitNetPrice)}
|
|
</div>
|
|
<div class="lineItem rightAlign">
|
|
${invoiceItem.vatPercentage}%
|
|
</div>
|
|
<div class="lineItem rightAlign">
|
|
${this.formatPrice(
|
|
invoiceItem.unitQuantity * invoiceItem.unitNetPrice
|
|
)}
|
|
</div>
|
|
</div>
|
|
`
|
|
)}
|
|
<div class="sums">
|
|
<div class="sumline">
|
|
<div class="label">
|
|
${this.translateKey("invoice@@sum.total.net")}
|
|
</div>
|
|
<div class="value value--total rightAlign">
|
|
${this.formatPrice(this.getTotalNet())}
|
|
</div>
|
|
</div>
|
|
${this.getVatGroups().map((vatGroupArg) => {
|
|
let itemNumbers = vatGroupArg.items
|
|
.map(
|
|
(item) =>
|
|
this.letterData.content.invoiceData.items.indexOf(item) + 1
|
|
)
|
|
.join(", ");
|
|
return html`
|
|
<div class="sumline">
|
|
<div class="label">
|
|
${this.translateKey("vat.short")}
|
|
${vatGroupArg.vatPercentage}%
|
|
${this.documentSettings.vatGroupPositions
|
|
? html`
|
|
<br /><span style="font-weight: normal"
|
|
>(${this.translateKey("invoice@@vat.position")}:
|
|
${itemNumbers})</span
|
|
>
|
|
`
|
|
: html``}
|
|
</div>
|
|
<div class="value rightAlign">
|
|
${this.formatPrice(vatGroupArg.vatAmountSum)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
})}
|
|
<div class="sumline">
|
|
<div class="label">${this.translateKey("invoice@@totalGross")}</div>
|
|
<div class="value value--total rightAlign">
|
|
${this.formatPrice(this.getTotalGross())}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="divider"></div>
|
|
|
|
${this.letterData?.content.invoiceData.reverseCharge
|
|
? html`<div class="taxNote">
|
|
${this.translateKey("invoice@@vat.reverseCharge.note")}
|
|
</div>`
|
|
: ``}
|
|
|
|
<!-- REFERENCED CONTRACT -->
|
|
${this.renderReferencedContract()}
|
|
|
|
<!-- PAYMENT TERMS -->
|
|
${this.renderPaymentTerms()}
|
|
|
|
<!-- PAYMENT INFO -->
|
|
${this.renderPaymentInfo()}
|
|
`,
|
|
contentNodes.currentContent
|
|
);
|
|
}
|
|
}
|