@design.estate/dees-document 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -124,6 +124,9 @@ interface IColumnLayout {
124
124
  flexibleMinWidth: number;
125
125
  }
126
126
 
127
+ /** the order of the VAT groups of one rate: taxed categories first, then the ones without tax */
128
+ const VAT_CATEGORY_ORDER: plugins.tsclass.finance.TVatCategory[] = ["S", "L", "M", "Z", "E", "AE", "K", "G", "O"];
129
+
127
130
  /**
128
131
  * The positions: number 40 px and VAT rate 50 px fixed, the description
129
132
  * flexible with at least 120 px (as the grid in the styles declares); the
@@ -608,9 +611,25 @@ export class DeContentInvoice extends DeesElement {
608
611
  return this.getTotalNet() + totalVat;
609
612
  };
610
613
 
614
+ /**
615
+ * The VAT category of an item (EN 16931 BT-151): the one it states, else
616
+ * reverse charge (AE) on a document that states reverse charge, else
617
+ * standard rated (S)
618
+ */
619
+ public vatCategoryOf = (
620
+ itemArg: plugins.tsclass.finance.TAccountingDocItem
621
+ ): plugins.tsclass.finance.TVatCategory =>
622
+ itemArg.vatCategory ?? (this.accountingDoc?.reverseCharge ? "AE" : "S");
623
+
624
+ /**
625
+ * The VAT groups of the document: one per VAT category and rate, so items
626
+ * that are zero rated, exempt or an intra-community supply at 0 % are not
627
+ * one line; the highest rate first
628
+ */
611
629
  public getVatGroups = () => {
612
630
  const vatGroups: {
613
631
  vatPercentage: number;
632
+ vatCategory: plugins.tsclass.finance.TVatCategory;
614
633
  items: plugins.tsclass.finance.TAccountingDocItem[];
615
634
  vatAmountSum: number;
616
635
  }[] = [];
@@ -620,33 +639,106 @@ export class DeContentInvoice extends DeesElement {
620
639
  return vatGroups;
621
640
  }
622
641
 
623
- const taxAmounts: number[] = [];
624
642
  for (const item of accountingDoc.items) {
625
- taxAmounts.includes(item.vatPercentage)
626
- ? null
627
- : taxAmounts.push(item.vatPercentage);
643
+ const vatCategory = this.vatCategoryOf(item);
644
+ const group = vatGroups.find(
645
+ (groupArg) => groupArg.vatCategory === vatCategory && groupArg.vatPercentage === item.vatPercentage
646
+ );
647
+ if (group) {
648
+ group.items.push(item);
649
+ } else {
650
+ vatGroups.push({ vatPercentage: item.vatPercentage, vatCategory, items: [item], vatAmountSum: 0 });
651
+ }
628
652
  }
629
653
 
630
- for (const taxAmount of taxAmounts) {
631
- const matchingItems = accountingDoc.items.filter(
632
- (itemArg) => itemArg.vatPercentage === taxAmount
633
- );
654
+ for (const group of vatGroups) {
634
655
  let sum = 0;
635
- for (const matchingItem of matchingItems) {
636
- sum +=
637
- matchingItem.unitNetPrice *
638
- matchingItem.unitQuantity *
639
- (taxAmount / 100);
656
+ for (const matchingItem of group.items) {
657
+ sum += matchingItem.unitNetPrice * matchingItem.unitQuantity * (group.vatPercentage / 100);
640
658
  }
641
- vatGroups.push({
642
- items: matchingItems,
643
- vatPercentage: taxAmount,
644
- vatAmountSum: Math.round(sum * 100) / 100,
645
- });
659
+ group.vatAmountSum = Math.round(sum * 100) / 100;
646
660
  }
647
- return vatGroups.sort((a, b) => b.vatPercentage - a.vatPercentage);
661
+ return vatGroups.sort(
662
+ (a, b) =>
663
+ b.vatPercentage - a.vatPercentage ||
664
+ VAT_CATEGORY_ORDER.indexOf(a.vatCategory) - VAT_CATEGORY_ORDER.indexOf(b.vatCategory)
665
+ );
648
666
  };
649
667
 
668
+ /**
669
+ * The label of a VAT group: the tax and its rate, and the category's name
670
+ * for a category other than standard rated and reverse charge (which has its
671
+ * statutory note below the sums); IGIC and IPSI are named instead of VAT
672
+ */
673
+ private vatGroupLabel(groupArg: { vatPercentage: number; vatCategory: plugins.tsclass.finance.TVatCategory }): string {
674
+ const category = groupArg.vatCategory;
675
+ if (category === "L" || category === "M") {
676
+ return `${this.translateKey(`vat.category.${category}`)} ${groupArg.vatPercentage}%`;
677
+ }
678
+ const rate = `${this.translateKey("vat.short")} ${groupArg.vatPercentage}%`;
679
+ return category === "S" || category === "AE" ? rate : `${rate} – ${this.translateKey(`vat.category.${category}`)}`;
680
+ }
681
+
682
+ /**
683
+ * The notes on the exemptions of the items (§ 14 Abs. 4 Satz 1 Nr. 8 UStG):
684
+ * the reason an item states (`vatExemptionReason`, EN 16931 BT-120), else,
685
+ * for an exempt item, an intra-community supply, an export or an item not
686
+ * subject to VAT, the words of its category. Items with the same note share
687
+ * it, with their positions. A reverse charge item is covered by the
688
+ * reverse-charge statement; a reason of its own that says something else is
689
+ * printed as well. The VATEX code (BT-121) is for machines and not printed.
690
+ */
691
+ private getExemptionNotes(): { note: string; positions: number[] }[] {
692
+ const notes: { note: string; positions: number[] }[] = [];
693
+ const statutory = new Set([
694
+ plugins.shared.translation.REVERSE_CHARGE_STATUTORY_PHRASE_DE,
695
+ this.translateKey("invoice@@vat.reverseCharge.note"),
696
+ ]);
697
+ (this.accountingDoc?.items ?? []).forEach((itemArg, index) => {
698
+ const category = this.vatCategoryOf(itemArg);
699
+ const stated = itemArg.vatExemptionReason?.trim();
700
+ const note =
701
+ stated ||
702
+ (category === "E" || category === "K" || category === "G" || category === "O"
703
+ ? this.translateKey(`vat.category.${category}.note`)
704
+ : undefined);
705
+ if (!note || (category === "AE" && statutory.has(note))) {
706
+ return;
707
+ }
708
+ const existing = notes.find((noteArg) => noteArg.note === note);
709
+ if (existing) {
710
+ existing.positions.push(index + 1);
711
+ } else {
712
+ notes.push({ note, positions: [index + 1] });
713
+ }
714
+ });
715
+ return notes;
716
+ }
717
+
718
+ /** The notes on the exemptions, each with its positions unless it applies to every item */
719
+ private renderExemptionNotes(): TemplateResult | null {
720
+ const notes = this.getExemptionNotes();
721
+ if (notes.length === 0) {
722
+ return null;
723
+ }
724
+ const itemCount = this.accountingDoc?.items.length ?? 0;
725
+ return html`<div class="taxNote">
726
+ ${notes.map(
727
+ (noteArg) => html`<div>
728
+ ${noteArg.note}${noteArg.positions.length < itemCount
729
+ ? html` (${this.translateKey("invoice@@vat.position")}: ${noteArg.positions.join(", ")})`
730
+ : null}
731
+ </div>`
732
+ )}
733
+ </div>`;
734
+ }
735
+
736
+ /** Whether the document or one of its items is in reverse charge */
737
+ private hasReverseCharge(): boolean {
738
+ const accountingDoc = this.accountingDoc;
739
+ return !!accountingDoc && (accountingDoc.reverseCharge || accountingDoc.items.some((itemArg) => itemArg.vatCategory === "AE"));
740
+ }
741
+
650
742
  /** the rendered shadow root; the element is always attached when the content is paginated */
651
743
  private get root(): ShadowRoot {
652
744
  const root = this.shadowRoot;
@@ -1409,8 +1501,7 @@ export class DeContentInvoice extends DeesElement {
1409
1501
  return html`
1410
1502
  <div class="sumline">
1411
1503
  <div class="label">
1412
- ${this.translateKey("vat.short")}
1413
- ${vatGroupArg.vatPercentage}%
1504
+ ${this.vatGroupLabel(vatGroupArg)}
1414
1505
  ${this.documentSettings.vatGroupPositions
1415
1506
  ? html`
1416
1507
  <br /><span style="font-weight: normal"
@@ -1437,7 +1528,8 @@ export class DeContentInvoice extends DeesElement {
1437
1528
  ${paidAmountSummary ? this.renderAdvancePayments(paidAmountSummary) : null}
1438
1529
  <div class="divider"></div>
1439
1530
 
1440
- ${accountingDoc?.reverseCharge ? this.renderReverseChargeNote() : ``}
1531
+ ${this.hasReverseCharge() ? this.renderReverseChargeNote() : ``}
1532
+ ${this.renderExemptionNotes()}
1441
1533
 
1442
1534
  <!-- REFERENCED CONTRACT -->
1443
1535
  ${this.renderReferencedContract()}