2023-10-16 16:28:12 +00:00
|
|
|
/**
|
|
|
|
* content for invoices
|
|
|
|
*/
|
|
|
|
|
|
|
|
import {
|
|
|
|
DeesElement,
|
|
|
|
property,
|
|
|
|
html,
|
|
|
|
customElement,
|
|
|
|
type TemplateResult,
|
|
|
|
css,
|
|
|
|
cssManager,
|
|
|
|
unsafeCSS,
|
|
|
|
render,
|
2024-11-30 19:54:15 +00:00
|
|
|
domtools,
|
2023-10-16 16:28:12 +00:00
|
|
|
} from '@design.estate/dees-element';
|
|
|
|
import * as plugins from '../plugins.js';
|
|
|
|
|
2024-12-05 19:20:29 +00:00
|
|
|
import { dedocumentSharedStyle } from '../style.js';
|
|
|
|
|
2023-10-16 16:28:12 +00:00
|
|
|
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;
|
|
|
|
|
2024-12-02 11:46:28 +00:00
|
|
|
@property({
|
|
|
|
type: Object,
|
|
|
|
reflect: true,
|
|
|
|
})
|
2024-12-02 15:04:58 +00:00
|
|
|
public documentSettings: plugins.shared.interfaces.IDocumentSettings;
|
2024-12-02 11:46:28 +00:00
|
|
|
|
2023-10-16 16:28:12 +00:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
domtools.DomTools.setupDomTools();
|
|
|
|
}
|
|
|
|
|
|
|
|
public static styles = [
|
|
|
|
domtools.elementBasic.staticStyles,
|
2024-12-05 19:20:29 +00:00
|
|
|
dedocumentSharedStyle,
|
2023-10-16 16:28:12 +00:00
|
|
|
css`
|
|
|
|
:host {
|
|
|
|
color: #333;
|
|
|
|
}
|
|
|
|
|
|
|
|
.trimmedContent {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
.repeatedContent {
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
];
|
|
|
|
|
|
|
|
public render(): TemplateResult {
|
|
|
|
return html`
|
|
|
|
<div class="trimmedContent"></div>
|
|
|
|
<div class="repeatedContent"></div>
|
|
|
|
<div class="currentContent"></div>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
|
|
|
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 async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
|
|
|
super.firstUpdated(_changedProperties);
|
|
|
|
this.attachInvoiceDom();
|
|
|
|
}
|
|
|
|
|
|
|
|
public async attachInvoiceDom() {
|
|
|
|
const contentNodes = await this.getContentNodes();
|
|
|
|
render(
|
|
|
|
html`
|
|
|
|
<style>
|
|
|
|
.grid {
|
|
|
|
display: grid;
|
2024-12-05 00:33:16 +00:00
|
|
|
grid-template-columns: 40px auto 60px 60px 84px 84px 46px;
|
2023-10-16 16:28:12 +00:00
|
|
|
}
|
|
|
|
.topLine {
|
|
|
|
margin-top: 5px;
|
|
|
|
background: #e7e7e7;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
.lineItem {
|
|
|
|
font-size: 12px;
|
|
|
|
padding: 5px;
|
2024-12-05 00:33:16 +00:00
|
|
|
border-right: 1px dashed #ccc;
|
|
|
|
}
|
|
|
|
|
2024-12-08 16:37:17 +00:00
|
|
|
.lineItem:last-child {
|
|
|
|
border-right: none;
|
|
|
|
}
|
|
|
|
|
2024-12-05 00:33:16 +00:00
|
|
|
.lineItem.rightAlign {
|
|
|
|
text-align: right;
|
2023-10-16 16:28:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.invoiceLine {
|
2024-12-08 16:37:17 +00:00
|
|
|
background: #ffffff00;
|
2023-10-16 16:28:12 +00:00
|
|
|
border-bottom: 1px dotted #ccc;
|
|
|
|
}
|
|
|
|
|
2024-12-08 16:37:17 +00:00
|
|
|
.invoiceLine.highlighted {
|
|
|
|
transition: background 0.2s;
|
|
|
|
background: #ffc18f;
|
|
|
|
border: 1px solid #ff9d4d;
|
|
|
|
box-sizing: content-box;
|
|
|
|
}
|
|
|
|
|
2023-10-16 16:28:12 +00:00
|
|
|
.sums {
|
|
|
|
margin-top: 5px;
|
|
|
|
font-size: 12px;
|
|
|
|
padding-left: 50%;
|
|
|
|
}
|
|
|
|
|
|
|
|
.sums .sumline {
|
|
|
|
margin-top: 3px;
|
|
|
|
display: grid;
|
|
|
|
grid-template-columns: auto 90px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.sums .sumline .label {
|
|
|
|
padding: 2px 5px;
|
|
|
|
border-right: 1px solid #ccc;
|
|
|
|
text-align: right;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
|
|
|
|
.sums .sumline .value {
|
|
|
|
padding: 2px 5px;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
|
|
|
|
.divider {
|
|
|
|
margin-top: 8px;
|
|
|
|
border-top: 1px dotted #ccc;
|
|
|
|
}
|
|
|
|
|
|
|
|
.taxNote {
|
|
|
|
font-size: 12px;
|
|
|
|
padding: 4px;
|
|
|
|
background: #eeeeeb;
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.infoBox {
|
|
|
|
border-radius: 7px;
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
border-left: 3px solid #c5e1a5;
|
|
|
|
padding: 8px;
|
|
|
|
margin-top: 16px;
|
|
|
|
line-height: 1.4em;
|
|
|
|
font-size: 16px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.infoBox .label {
|
|
|
|
padding-bottom: 2px;
|
|
|
|
font-size: 12px;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
|
|
|
|
.paymentCode {
|
|
|
|
text-align: center;
|
|
|
|
border-left: 2px solid #666;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
<div>We hereby invoice products and services provided to you by Lossless GmbH:</div>
|
|
|
|
<div class="grid topLine dataHeader">
|
2024-12-08 16:37:17 +00:00
|
|
|
<div class="lineItem rightAlign">
|
|
|
|
${plugins.shared.translation.translate(
|
|
|
|
this.documentSettings.languageCode,
|
|
|
|
'itemPos',
|
|
|
|
'Item Pos.'
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<div class="lineItem">
|
|
|
|
${plugins.shared.translation.translate(
|
|
|
|
this.documentSettings.languageCode,
|
|
|
|
'description',
|
|
|
|
'Description'
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<div class="lineItem rightAlign">
|
|
|
|
${plugins.shared.translation.translate(
|
|
|
|
this.documentSettings.languageCode,
|
|
|
|
'quantity',
|
|
|
|
'Quantity'
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<div class="lineItem">
|
|
|
|
${plugins.shared.translation.translate(
|
|
|
|
this.documentSettings.languageCode,
|
|
|
|
'unitType',
|
|
|
|
'Unit Type'
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<div class="lineItem rightAlign">
|
|
|
|
${plugins.shared.translation.translate(
|
|
|
|
this.documentSettings.languageCode,
|
|
|
|
'unitNetPrice',
|
|
|
|
'Unit Net Price'
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<div class="lineItem rightAlign">
|
|
|
|
${plugins.shared.translation.translate(
|
|
|
|
this.documentSettings.languageCode,
|
|
|
|
'totalNetPrice',
|
|
|
|
'Total Net Price'
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<div class="lineItem rightAlign">
|
|
|
|
${plugins.shared.translation.translate(
|
|
|
|
this.documentSettings.languageCode,
|
|
|
|
'vatShort',
|
|
|
|
'VAT'
|
|
|
|
)}
|
|
|
|
</div>
|
2023-10-16 16:28:12 +00:00
|
|
|
</div>
|
|
|
|
${(() => {
|
|
|
|
let counter = 1;
|
2024-12-08 16:37:17 +00:00
|
|
|
return this.letterData?.content.invoiceData?.items?.map((invoiceItem) => {
|
|
|
|
const isHighlighted = false; // TODO: implement rest of highlight logic
|
|
|
|
return html`
|
|
|
|
<div class="grid invoiceLine needsDataHeader ${isHighlighted ? 'highlighted' : ''}">
|
2024-12-05 00:33:16 +00:00
|
|
|
<div class="lineItem rightAlign">${counter++}</div>
|
2023-10-16 16:28:12 +00:00
|
|
|
<div class="lineItem">${invoiceItem.name}</div>
|
2024-12-05 00:33:16 +00:00
|
|
|
<div class="lineItem rightAlign">${invoiceItem.unitQuantity}</div>
|
2023-10-16 16:28:12 +00:00
|
|
|
<div class="lineItem">${invoiceItem.unitType}</div>
|
2024-12-05 00:33:16 +00:00
|
|
|
<div class="lineItem rightAlign">
|
2024-12-08 16:37:17 +00:00
|
|
|
${invoiceItem.unitNetPrice} ${this.letterData?.content.invoiceData.currency}
|
2023-10-16 16:28:12 +00:00
|
|
|
</div>
|
2024-12-05 00:33:16 +00:00
|
|
|
<div class="lineItem rightAlign">
|
2024-12-08 16:37:17 +00:00
|
|
|
${invoiceItem.unitQuantity * invoiceItem.unitNetPrice}
|
|
|
|
${this.letterData?.content.invoiceData.currency}
|
2024-12-05 00:33:16 +00:00
|
|
|
</div>
|
2024-12-08 16:37:17 +00:00
|
|
|
<div class="lineItem rightAlign">${invoiceItem.vatPercentage}%</div>
|
2023-10-16 16:28:12 +00:00
|
|
|
</div>
|
2024-12-08 16:37:17 +00:00
|
|
|
`;
|
|
|
|
});
|
2023-10-16 16:28:12 +00:00
|
|
|
})()}
|
|
|
|
<div class="sums">
|
|
|
|
<div class="sumline">
|
|
|
|
<div class="label">Total net</div>
|
|
|
|
<div class="value">${this.getTotalNet()} EUR</div>
|
|
|
|
</div>
|
|
|
|
${this.getVatGroups().map((vatGroupArg) => {
|
|
|
|
let itemNumbers = '';
|
2024-12-05 19:23:19 +00:00
|
|
|
let first = true;
|
2023-10-16 16:28:12 +00:00
|
|
|
for (const item of vatGroupArg.items) {
|
|
|
|
const itemIndex = this.letterData.content.invoiceData.items.indexOf(item);
|
2024-12-05 19:23:19 +00:00
|
|
|
itemNumbers += `${first ? '' : ', '}${itemIndex + 1}`;
|
|
|
|
first = false;
|
2023-10-16 16:28:12 +00:00
|
|
|
}
|
|
|
|
return html`
|
|
|
|
<div class="sumline">
|
|
|
|
<div class="label">
|
2024-12-05 00:33:16 +00:00
|
|
|
Vat ${vatGroupArg.vatPercentage}%
|
2024-12-08 16:37:17 +00:00
|
|
|
${this.documentSettings.vatGroupPositions
|
|
|
|
? html`
|
|
|
|
<br /><span style="font-weight: normal"
|
|
|
|
>(on item positions: ${itemNumbers})</span
|
|
|
|
>
|
|
|
|
`
|
|
|
|
: html``}
|
2023-10-16 16:28:12 +00:00
|
|
|
</div>
|
|
|
|
<div class="value">${vatGroupArg.vatAmountSum} EUR</div>
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
})}
|
|
|
|
<div class="sumline">
|
|
|
|
<div class="label">Total gross</div>
|
|
|
|
<div class="value">${this.getTotalGross()} EUR</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="divider"></div>
|
|
|
|
${this.letterData?.content.invoiceData.reverseCharge
|
|
|
|
? html`
|
|
|
|
<div class="taxNote">
|
2024-12-08 16:37:17 +00:00
|
|
|
${plugins.shared.translation.translate(
|
|
|
|
this.documentSettings.languageCode,
|
|
|
|
'reverseVatNote',
|
|
|
|
'VAT arises on a reverse charge basis and is payable by the customer.'
|
|
|
|
)}
|
2023-10-16 16:28:12 +00:00
|
|
|
</div>
|
|
|
|
`
|
|
|
|
: ``}
|
|
|
|
<div class="infoBox">
|
|
|
|
<div class="label">Payment Terms:</div>
|
|
|
|
Payment is due within 30 days starting from the reception of this invoice. Please use the
|
|
|
|
following SEPA details:
|
|
|
|
<br /><br />
|
|
|
|
Beneficiary: ${this.letterData?.from.name}<br />
|
|
|
|
IBAN: ${this.letterData?.from?.sepaConnection?.iban}<br />
|
|
|
|
BIC: ${this.letterData?.from?.sepaConnection?.bic}<br />
|
|
|
|
Description: ${this.letterData?.content.invoiceData?.id}<br />
|
2024-11-27 18:15:29 +00:00
|
|
|
Amount: ${this.getTotalGross()} ${this.letterData?.content.invoiceData.currency}
|
2023-10-16 16:28:12 +00:00
|
|
|
</div>
|
|
|
|
${this.letterData?.content?.contractData?.contractDate
|
|
|
|
? html`
|
|
|
|
<div class="infoBox">
|
|
|
|
<div class="label">Referenced contract:</div>
|
|
|
|
This invoice is adhering to agreements made by contract between the parties on
|
2024-12-08 16:37:17 +00:00
|
|
|
${plugins.smarttime.ExtendedDate.fromMillis(
|
|
|
|
this.letterData?.content.contractData.contractDate
|
|
|
|
).format('MMMM D, YYYY')}.
|
2023-10-16 16:28:12 +00:00
|
|
|
</div>
|
|
|
|
`
|
|
|
|
: html``}
|
|
|
|
<div class="infoBox paymentCode">
|
|
|
|
<div class="label">Sepa Payment Code:</div>
|
|
|
|
</div>
|
|
|
|
`,
|
|
|
|
contentNodes.currentContent
|
|
|
|
);
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
plugins.qrcode.toCanvas(
|
|
|
|
canvas,
|
|
|
|
`BCD
|
|
|
|
001
|
|
|
|
1
|
|
|
|
SCT
|
|
|
|
${this.letterData.content.invoiceData.billedBy.sepaConnection.bic}
|
|
|
|
${this.letterData.content.invoiceData.billedBy.name}
|
|
|
|
${this.letterData.content.invoiceData.billedBy.sepaConnection.iban}
|
|
|
|
EUR${this.getTotalGross()}
|
|
|
|
CHAR
|
|
|
|
${this.letterData.content.invoiceData.id}
|
|
|
|
${this.letterData.content.invoiceData.id}
|
|
|
|
EPC QR Code`,
|
|
|
|
(error) => {
|
|
|
|
if (error) console.error(error);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
contentNodes.currentContent.querySelector('.paymentCode').append(canvas);
|
|
|
|
}
|
|
|
|
}
|