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
 | |
|     );
 | |
|   }
 | |
| }
 |