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' ;
2024-11-30 19:54:15 +00:00
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 > < / d e d o c u m e n t - c o n t e n t i n v o i c e >
< / 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 60 px 60 px 84 px 84 px 46 px ;
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 ;
}
. lineItem . rightAlign {
text - align : right ;
2023-10-16 16:28:12 +00:00
}
. 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 90 px ;
}
. sums . sumline . label {
padding : 2px 5 px ;
border - right : 1px solid # ccc ;
text - align : right ;
font - weight : bold ;
}
. sums . sumline . value {
padding : 2px 5 px ;
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-05 00:33:16 +00:00
< div class = "lineItem rightAlign" > $ { plugins . shared . translation . translate ( this . documentSettings . languageCode , 'itemPos' , 'Item Pos.' ) } < / div >
2024-12-02 15:04:58 +00:00
< div class = "lineItem" > $ { plugins . shared . translation . translate ( this . documentSettings . languageCode , 'description' , 'Description' ) } < / div >
2024-12-05 00:33:16 +00:00
< div class = "lineItem rightAlign" > $ { plugins . shared . translation . translate ( this . documentSettings . languageCode , 'quantity' , 'Quantity' ) } < / div >
2024-12-02 15:04:58 +00:00
< div class = "lineItem" > $ { plugins . shared . translation . translate ( this . documentSettings . languageCode , 'unitType' , 'Unit Type' ) } < / div >
2024-12-05 00:33:16 +00:00
< 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 ;
return this . letterData ? . content . invoiceData ? . items ? . map (
( invoiceItem ) = > html `
< div class = "grid invoiceLine needsDataHeader" >
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" > $ { invoiceItem . unitNetPrice } $ { this . letterData ? . content . invoiceData . currency } < / div >
< div class = "lineItem rightAlign" >
2024-11-27 18:15:29 +00:00
$ { invoiceItem . unitQuantity * 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" >
$ { invoiceItem . vatPercentage } %
< / div >
2023-10-16 16:28:12 +00:00
< / 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 = '' ;
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 } %
$ { 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-02 15:04:58 +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
$ { 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 ) ;
}
) ;
contentNodes . currentContent . querySelector ( '.paymentCode' ) . append ( canvas ) ;
}
}