386 lines
11 KiB
TypeScript
386 lines
11 KiB
TypeScript
|
/**
|
||
|
* content for invoices
|
||
|
*/
|
||
|
|
||
|
import {
|
||
|
DeesElement,
|
||
|
property,
|
||
|
html,
|
||
|
customElement,
|
||
|
type TemplateResult,
|
||
|
css,
|
||
|
cssManager,
|
||
|
unsafeCSS,
|
||
|
render,
|
||
|
} from '@design.estate/dees-element';
|
||
|
import * as domtools from '@design.estate/dees-domtools';
|
||
|
|
||
|
import * as plugins from '../plugins.js';
|
||
|
|
||
|
import * as shared from './shared/index.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;
|
||
|
|
||
|
constructor() {
|
||
|
super();
|
||
|
domtools.DomTools.setupDomTools();
|
||
|
}
|
||
|
|
||
|
public static styles = [
|
||
|
domtools.elementBasic.staticStyles,
|
||
|
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)
|
||
|
);
|
||
|
console.log('hey' + this.shadowRoot.children.length);
|
||
|
}
|
||
|
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;
|
||
|
grid-template-columns: 40px auto 80px 80px 90px 90px;
|
||
|
}
|
||
|
.topLine {
|
||
|
margin-top: 5px;
|
||
|
background: #e7e7e7;
|
||
|
font-weight: bold;
|
||
|
}
|
||
|
.lineItem {
|
||
|
font-size: 12px;
|
||
|
padding: 5px;
|
||
|
}
|
||
|
|
||
|
.invoiceLine {
|
||
|
border-bottom: 1px dotted #ccc;
|
||
|
}
|
||
|
|
||
|
.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">
|
||
|
<div class="lineItem">Item Pos.</div>
|
||
|
<div class="lineItem">Description</div>
|
||
|
<div class="lineItem">Quantity</div>
|
||
|
<div class="lineItem">Unit Type</div>
|
||
|
<div class="lineItem">Unit Net Price</div>
|
||
|
<div class="lineItem">Total Net Price</div>
|
||
|
</div>
|
||
|
${(() => {
|
||
|
let counter = 1;
|
||
|
return this.letterData?.content.invoiceData?.items?.map(
|
||
|
(invoiceItem) => html`
|
||
|
<div class="grid invoiceLine needsDataHeader">
|
||
|
<div class="lineItem">${counter++}</div>
|
||
|
<div class="lineItem">${invoiceItem.name}</div>
|
||
|
<div class="lineItem">${invoiceItem.unitQuantity}</div>
|
||
|
<div class="lineItem">${invoiceItem.unitType}</div>
|
||
|
<div class="lineItem">${invoiceItem.unitNetPrice} ${invoiceItem.currency}</div>
|
||
|
<div class="lineItem">
|
||
|
${invoiceItem.unitQuantity * invoiceItem.unitNetPrice} ${invoiceItem.currency}
|
||
|
</div>
|
||
|
</div>
|
||
|
`
|
||
|
);
|
||
|
})()}
|
||
|
<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 = '';
|
||
|
for (const item of vatGroupArg.items) {
|
||
|
const itemIndex = this.letterData.content.invoiceData.items.indexOf(item);
|
||
|
itemNumbers += ` ${itemIndex + 1},`;
|
||
|
}
|
||
|
return html`
|
||
|
<div class="sumline">
|
||
|
<div class="label">
|
||
|
Vat ${vatGroupArg.vatPercentage}%<br />
|
||
|
<span style="font-weight: normal">(on item positions: ${itemNumbers})</span>
|
||
|
</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">
|
||
|
VAT arises on a reverse charge basis and is payable by the customer.
|
||
|
</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 />
|
||
|
Amount: ${this.getTotalGross()} ${this.letterData?.content.invoiceData.items[0].currency}
|
||
|
</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
|
||
|
${plugins.smarttime.ExtendedDate.fromMillis(this.letterData?.content.contractData.contractDate).format('MMMM D, YYYY')}.
|
||
|
</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);
|
||
|
console.log('success!');
|
||
|
}
|
||
|
);
|
||
|
contentNodes.currentContent.querySelector('.paymentCode').append(canvas);
|
||
|
}
|
||
|
}
|